Skip to content

Commit 712eddb

Browse files
authored
Merge pull request #357 from Art4/add-httpclient
[poc] new low-level API client
2 parents 6efb402 + 63dfce8 commit 712eddb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1166
-202
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased](https://github.com/kbsali/php-redmine-api/compare/v2.4.0...v2.x)
99

10+
### Changed
11+
12+
- The last response is saved in `Redmine\Api\AbstractApi` to prevent race conditions with `Redmine\Client\Client` implementations.
13+
1014
## [v2.4.0](https://github.com/kbsali/php-redmine-api/compare/v2.3.0...v2.4.0) - 2024-01-04
1115

1216
### Added

composer.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@
4949
"codestyle": "php-cs-fixer fix",
5050
"coverage": "phpunit --coverage-html=\".phpunit.cache/code-coverage\"",
5151
"phpstan": "phpstan analyze --memory-limit 512M --configuration .phpstan.neon",
52-
"test": "phpunit"
52+
"phpunit": "phpunit",
53+
"test": [
54+
"@phpstan",
55+
"@phpunit",
56+
"@codestyle --dry-run --diff"
57+
]
5358
}
5459
}

src/Redmine/Api/AbstractApi.php

Lines changed: 147 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22

33
namespace Redmine\Api;
44

5+
use Closure;
6+
use InvalidArgumentException;
57
use Redmine\Api;
68
use Redmine\Client\Client;
79
use Redmine\Exception;
810
use Redmine\Exception\SerializerException;
11+
use Redmine\Http\HttpClient;
12+
use Redmine\Http\Response;
913
use Redmine\Serializer\JsonSerializer;
1014
use Redmine\Serializer\PathSerializer;
1115
use Redmine\Serializer\XmlSerializer;
@@ -23,9 +27,52 @@ abstract class AbstractApi implements Api
2327
*/
2428
protected $client;
2529

26-
public function __construct(Client $client)
30+
/**
31+
* @var HttpClient
32+
*/
33+
private $httpClient;
34+
35+
/**
36+
* @var Response
37+
*/
38+
private $lastResponse;
39+
40+
/**
41+
* @param Client|HttpClient $client
42+
*/
43+
public function __construct($client)
2744
{
28-
$this->client = $client;
45+
if (! is_object($client) || (! $client instanceof Client && ! $client instanceof HttpClient)) {
46+
throw new InvalidArgumentException(sprintf(
47+
'%s(): Argument #1 ($client) must be of type %s or %s, `%s` given',
48+
__METHOD__,
49+
Client::class,
50+
HttpClient::class,
51+
(is_object($client)) ? get_class($client) : gettype($client)
52+
));
53+
}
54+
55+
if ($client instanceof Client) {
56+
$this->client = $client;
57+
}
58+
59+
$httpClient = $client;
60+
61+
if (! $httpClient instanceof HttpClient) {
62+
$httpClient = $this->handleClient($client);
63+
}
64+
65+
$this->httpClient = $httpClient;
66+
}
67+
68+
final protected function getHttpClient(): HttpClient
69+
{
70+
return $this->httpClient;
71+
}
72+
73+
final protected function getLastResponse(): Response
74+
{
75+
return $this->lastResponse !== null ? $this->lastResponse : $this->createResponse(0, '', '');
2976
}
3077

3178
/**
@@ -40,7 +87,13 @@ public function lastCallFailed()
4087
{
4188
@trigger_error('`' . __METHOD__ . '()` is deprecated since v2.1.0, use \Redmine\Client\Client::getLastResponseStatusCode() instead.', E_USER_DEPRECATED);
4289

43-
$code = $this->client->getLastResponseStatusCode();
90+
if ($this->lastResponse !== null) {
91+
$code = $this->lastResponse->getStatusCode();
92+
} elseif ($this->client !== null) {
93+
$code = $this->client->getLastResponseStatusCode();
94+
} else {
95+
$code = 0;
96+
}
4497

4598
return 200 !== $code && 201 !== $code;
4699
}
@@ -55,10 +108,10 @@ public function lastCallFailed()
55108
*/
56109
protected function get($path, $decodeIfJson = true)
57110
{
58-
$this->client->requestGet(strval($path));
111+
$this->lastResponse = $this->getHttpClient()->request('GET', strval($path));
59112

60-
$body = $this->client->getLastResponseBody();
61-
$contentType = $this->client->getLastResponseContentType();
113+
$body = $this->lastResponse->getBody();
114+
$contentType = $this->lastResponse->getContentType();
62115

63116
// if response is XML, return a SimpleXMLElement object
64117
if ('' !== $body && 0 === strpos($contentType, 'application/xml')) {
@@ -82,16 +135,17 @@ protected function get($path, $decodeIfJson = true)
82135
* @param string $path
83136
* @param string $data
84137
*
85-
* @return string|false
138+
* @return string|SimpleXMLElement|false
86139
*/
87140
protected function post($path, $data)
88141
{
89-
$this->client->requestPost($path, $data);
142+
$this->lastResponse = $this->getHttpClient()->request('POST', strval($path), $data);
90143

91-
$body = $this->client->getLastResponseBody();
144+
$body = $this->lastResponse->getBody();
145+
$contentType = $this->lastResponse->getContentType();
92146

93147
// if response is XML, return a SimpleXMLElement object
94-
if ('' !== $body && 0 === strpos($this->client->getLastResponseContentType(), 'application/xml')) {
148+
if ('' !== $body && 0 === strpos($contentType, 'application/xml')) {
95149
return new SimpleXMLElement($body);
96150
}
97151

@@ -104,16 +158,17 @@ protected function post($path, $data)
104158
* @param string $path
105159
* @param string $data
106160
*
107-
* @return string|false
161+
* @return string|SimpleXMLElement
108162
*/
109163
protected function put($path, $data)
110164
{
111-
$this->client->requestPut($path, $data);
165+
$this->lastResponse = $this->getHttpClient()->request('PUT', strval($path), $data);
112166

113-
$body = $this->client->getLastResponseBody();
167+
$body = $this->lastResponse->getBody();
168+
$contentType = $this->lastResponse->getContentType();
114169

115170
// if response is XML, return a SimpleXMLElement object
116-
if ('' !== $body && 0 === strpos($this->client->getLastResponseContentType(), 'application/xml')) {
171+
if ('' !== $body && 0 === strpos($contentType, 'application/xml')) {
117172
return new SimpleXMLElement($body);
118173
}
119174

@@ -125,13 +180,13 @@ protected function put($path, $data)
125180
*
126181
* @param string $path
127182
*
128-
* @return false|SimpleXMLElement|string
183+
* @return string
129184
*/
130185
protected function delete($path)
131186
{
132-
$this->client->requestDelete($path);
187+
$this->lastResponse = $this->getHttpClient()->request('DELETE', strval($path));
133188

134-
return $this->client->getLastResponseBody();
189+
return $this->lastResponse->getBody();
135190
}
136191

137192
/**
@@ -179,7 +234,7 @@ protected function retrieveAll($endpoint, array $params = [])
179234
try {
180235
$data = $this->retrieveData(strval($endpoint), $params);
181236
} catch (Exception $e) {
182-
if ($this->client->getLastResponseBody() === '') {
237+
if ($this->getLastResponse()->getBody() === '') {
183238
return false;
184239
}
185240

@@ -203,9 +258,9 @@ protected function retrieveAll($endpoint, array $params = [])
203258
protected function retrieveData(string $endpoint, array $params = []): array
204259
{
205260
if (empty($params)) {
206-
$this->client->requestGet($endpoint);
261+
$this->lastResponse = $this->getHttpClient()->request('GET', strval($endpoint));
207262

208-
return $this->getLastResponseBodyAsArray();
263+
return $this->getResponseAsArray($this->lastResponse);
209264
}
210265

211266
$params = $this->sanitizeParams(
@@ -232,11 +287,12 @@ protected function retrieveData(string $endpoint, array $params = []): array
232287
$params['limit'] = $_limit;
233288
$params['offset'] = $offset;
234289

235-
$this->client->requestGet(
290+
$this->lastResponse = $this->getHttpClient()->request(
291+
'GET',
236292
PathSerializer::create($endpoint, $params)->getPath()
237293
);
238294

239-
$newDataSet = $this->getLastResponseBodyAsArray();
295+
$newDataSet = $this->getResponseAsArray($this->lastResponse);
240296

241297
$returnData = array_merge_recursive($returnData, $newDataSet);
242298

@@ -310,11 +366,10 @@ protected function attachCustomFieldXML(SimpleXMLElement $xml, array $fields)
310366
*
311367
* @throws SerializerException if response body could not be converted into array
312368
*/
313-
private function getLastResponseBodyAsArray(): array
369+
private function getResponseAsArray(Response $response): array
314370
{
315-
$body = $this->client->getLastResponseBody();
316-
317-
$contentType = $this->client->getLastResponseContentType();
371+
$body = $response->getBody();
372+
$contentType = $response->getContentType();
318373
$returnData = null;
319374

320375
// parse XML
@@ -330,4 +385,70 @@ private function getLastResponseBodyAsArray(): array
330385

331386
return $returnData;
332387
}
388+
389+
private function handleClient(Client $client): HttpClient
390+
{
391+
$responseFactory = Closure::fromCallable([$this, 'createResponse']);
392+
393+
return new class ($client, $responseFactory) implements HttpClient {
394+
private $client;
395+
private $responseFactory;
396+
397+
public function __construct(Client $client, Closure $responseFactory)
398+
{
399+
$this->client = $client;
400+
$this->responseFactory = $responseFactory;
401+
}
402+
403+
public function request(string $method, string $path, string $body = ''): Response
404+
{
405+
if ($method === 'POST') {
406+
$this->client->requestPost($path, $body);
407+
} elseif ($method === 'PUT') {
408+
$this->client->requestPut($path, $body);
409+
} elseif ($method === 'DELETE') {
410+
$this->client->requestDelete($path);
411+
} else {
412+
$this->client->requestGet($path);
413+
}
414+
415+
return ($this->responseFactory)(
416+
$this->client->getLastResponseStatusCode(),
417+
$this->client->getLastResponseContentType(),
418+
$this->client->getLastResponseBody()
419+
);
420+
}
421+
};
422+
}
423+
424+
private function createResponse(int $statusCode, string $contentType, string $body): Response
425+
{
426+
return new class ($statusCode, $contentType, $body) implements Response {
427+
private $statusCode;
428+
private $contentType;
429+
private $body;
430+
431+
public function __construct(int $statusCode, string $contentType, string $body)
432+
{
433+
$this->statusCode = $statusCode;
434+
$this->contentType = $contentType;
435+
$this->body = $body;
436+
}
437+
438+
public function getStatusCode(): int
439+
{
440+
return $this->statusCode;
441+
}
442+
443+
public function getContentType(): string
444+
{
445+
return $this->contentType;
446+
}
447+
448+
public function getBody(): string
449+
{
450+
return $this->body;
451+
}
452+
};
453+
}
333454
}

src/Redmine/Api/CustomField.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public function all(array $params = [])
5555
try {
5656
$this->customFields = $this->list($params);
5757
} catch (Exception $e) {
58-
if ($this->client->getLastResponseBody() === '') {
58+
if ($this->getLastResponse()->getBody() === '') {
5959
return false;
6060
}
6161

src/Redmine/Api/Group.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public function all(array $params = [])
5858
try {
5959
$this->groups = $this->list($params);
6060
} catch (Exception $e) {
61-
if ($this->client->getLastResponseBody() === '') {
61+
if ($this->getLastResponse()->getBody() === '') {
6262
return false;
6363
}
6464

0 commit comments

Comments
 (0)