Skip to content

Commit 39285b0

Browse files
authored
Update docs for Psr18Client (#260) @Art4
* Create Psr18Client * Create interfaces for Client and Api Move Api instantiation into trait * Issue Api uses getApi on client * Add new request methods to ClientInterface * Add tests for new ClientInterface methods * Add test for requestGet method * Test Request created by client * Move tests for request generation into integration test * Improve integration tests * Move clients into own namespace, rename interfaces * Implement auth with username:pwd or access key * move Api interface * add content-type header * Let AbstractApi consume new Client interface Move json- and XML-decoding into AbtractApi Fix and improve all tests * test exception messages * Implement impersonate user * Test correct response data in client * Add tests for AbstractApi * Add tests for decoding in AbstractApi Simplify error messages in JSON decoding * Test XML decoding on post, put and delete requests * Add integration tests for POST, PUT and DELETE * Test file upload with content and file path * Remove debug code * Update example.php to use new client interface * Update README.md * Create usage docs * Add docs for user impersonation and curl options, improve language * Improve AbstractApi, remove obvious docblocks rename $data to $body rename $decode to $decodeIfJson * Rephrase trait description, add @internal * Update manual installation, link to docs * Add tests for get api over magic getter * Update requirements in README.md * Remove redundant impersonate user section * Fix linkage to example.php * Add CHANGELOG.md * Move example.php to docs/usage.md * Create migration guide * Descripe in migration guide how to use the new methods * fix code examples * Add example for switching to new client * simplify example * Finish migration guide
1 parent 238cef9 commit 39285b0

File tree

4 files changed

+534
-57
lines changed

4 files changed

+534
-57
lines changed

CHANGELOG.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [Unreleased](https://github.com/kbsali/php-redmine-api/compare/v1.7.0...master)
9+
10+
### Added
11+
12+
- This `CHANGELOG.md` file
13+
14+
## [v1.7.0](https://github.com/kbsali/php-redmine-api/compare/v1.6.0...v1.7.0) - 2021-03-22
15+
16+
### Added
17+
18+
- New interface `Redmine\Client\Client` for all clients
19+
- New PSR-18 based client `Redmine\Client\Psr18Client` for usage with e.g. `Guzzle`
20+
- New method `Redmine\Client::getApi()` for returning an API instance, `Redmine\Client::api()` and magic getter `Redmine\Client->issue` will be deprecated in future.
21+
- New method `Redmine\Client::startImpersonateUser()` to set an impersonated user, `Redmine\Client::setImpersonateUser()` will be deprecated in future.
22+
- New method `Redmine\Client::stopImpersonateUser()` to stop impersonating an user.
23+
- New method `Redmine\Client::requestGet()` to create and send a GET request, `Redmine\Client::get()` will be deprecated in future.
24+
- New method `Redmine\Client::requestPost()` to create and send a POST request, `Redmine\Client::post()` will be deprecated in future.
25+
- New method `Redmine\Client::requestPut()` to create and send a PUT request, `Redmine\Client::put()` will be deprecated in future.
26+
- New method `Redmine\Client::requestDelete()` to create and send a DELETE request, `Redmine\Client::delete()` will be deprecated in future.
27+
- New method `Redmine\Client::getLastResponseStatusCode()` returns status code of the last response, `Redmine\Client::getResponseCode()` will be deprecated in future.
28+
- New method `Redmine\Client::getLastResponseContentType()` returns the content type of the last response.
29+
- New method `Redmine\Client::getLastResponseBody()` returns the raw body of the last response.
30+
31+
### Changed
32+
33+
- Move JSON and XML decoding directly into `Redmine\Api\AbstractApi` instead of the client.
34+
35+
### Fixed
36+
37+
- escape special chars in title, description, etc in wiki, issue, project and time_entry api.
38+
39+
## [v1.6.0](https://github.com/kbsali/php-redmine-api/compare/v1.5.22...v1.6.0) - 2021-01-02
40+
41+
### Added
42+
43+
- Added support for PHP 8.0
44+
- New method `Redmine\Api\Attachment::remove()` to delete an attachment
45+
- New method `Redmine\Api\TimeEntryActivity::listing()` to list time entry activities
46+
- New method `Redmine\Api\TimeEntryActivity::getIdByName()` to get a time entry activity id by its name
47+
48+
### Removed
49+
50+
- Removed support for PHP 5.6, 7.0, 7.1 and 7.2
51+
52+
## [v1.5.22](https://github.com/kbsali/php-redmine-api/compare/v1.5.21...v1.5.22) - 2021-01-02
53+
54+
### Added
55+
56+
- Added support for filename parameter to attachment upload
57+
- Added file upload with wiki
58+
59+
### Fixed
60+
61+
- Fixed a warning on file upload
62+
- Fixed a lot of warnings related to `custom_field`
63+
- Fixed custom field file type

README.md

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ Create your project e.g. in the `index.php` by require the `vendor/autoload.php`
143143

144144
You can choose between the navite curl client or the PSR-18 compatible client.
145145

146+
> :bulb: Since `php-redmine-api` v1.7.0 there is a new PSR-18 based client `Redmine\Client\Psr18Client`. [See this guide if you want to switch to this client.](docs/migrate-to-psr18client.md).
147+
146148
#### Native curl Client `Redmine\Client`
147149

148150
Every Client requires a URL to your Redmine instance and either a valid Apikey...
@@ -305,26 +307,7 @@ $client->getApi('issue')->all([
305307
]);
306308
```
307309

308-
See `[example.php](example.php)` for further examples.
309-
310-
## User Impersonation
311-
312-
As of Redmine V2.2 you can impersonate user through the REST API :
313-
314-
```php
315-
316-
$client = new Redmine\Client('http://redmine.example.com', 'API_ACCESS_KEY');
317-
318-
// impersonate user
319-
$client->startImpersonateUser('jsmith');
320-
321-
// create a time entry for jsmith
322-
$client->getApi('time_entry')->create($data);
323-
324-
// remove impersonation for further calls
325-
$client->stopImpersonateUser();
326-
```
327-
310+
[See further examples and read more about usage in the docs](docs/usage.md).
328311

329312
### Thanks!
330313

docs/migrate-to-psr18client.md

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
# Migrate from `Redmine\Client` to `Redmine\Client\Psr18Client`
2+
3+
Since `php-redmine-api` v1.7.0 there is a new PSR-18 based client `Redmine\Client\Psr18Client`. This guide will help you to migrate your code if you want to use an app-wide PSR-18 HTTP client.
4+
5+
## 1. Use new client methods
6+
7+
With the new interface `Redmine\Client\Client` there are now standarized methods for all clients. The new `Redmine\Client\Psr18Client` and the current `Redmine\Client` implementing this interface.
8+
9+
### api() to getApi()
10+
11+
Search in your code for the usage of `$client->api('issue')` and the magic getter like `$client->issue`. Then replace this calls with `$client->getApi('issue')`.
12+
13+
```diff
14+
-$issue = $client->issue->show($issueId);
15+
+$issue = $client->getApi('issue')->show($issueId);
16+
17+
-$client->api('issue')->create($data);
18+
+$client->getApi('issue')->create($data);
19+
```
20+
21+
### getResponseCode() to getLastResponseStatusCode()
22+
23+
Replace every call for `$client->getResponseCode()` with `$client->getLastResponseStatusCode()`.
24+
25+
```diff
26+
-if ($client->getResponseCode() === 500)
27+
+if ($client->getLastResponseStatusCode() === 500)
28+
{
29+
throw new \Exception('Redmine call failed');
30+
}
31+
```
32+
33+
### get() to requestGet()
34+
35+
If you are using `$client->get()`, `$client->post()`, `$client->put()` or `$client->delete()` directly you will have to change your code. This methods parse a possible JSON or XML response but in future the parsing of the raw response body will be up to you.
36+
37+
To help you with the parsing of the raw response the client interface introduces two new methods: `getLastResponseContentType()` and `getLastResponseBody()`.
38+
39+
This example shows how you can parse the response body of a GET request.
40+
41+
```diff
42+
-// We dont know if we will get json, xml or a string
43+
-$dataAsJsonOrXmlOrString = $this->client->get($path);
44+
+$this->client->requestGet($path);
45+
+// $body contains the raw http body of the response
46+
+$body = $this->client->getLastResponseBody();
47+
+
48+
+// if response is XML, create a SimpleXMLElement object
49+
+if ($body !== '' && 0 === strpos($this->client->getLastResponseContentType(), 'application/xml')) {
50+
+ $dataAsXML = new \SimpleXMLElement($body);
51+
+} else if ($body !== '' && 0 === strpos($this->client->getLastResponseContentType(), 'application/json')) {
52+
+ try {
53+
+ $dataAsJson = json_decode($body, true, 512, \JSON_THROW_ON_ERROR);
54+
+ } catch (\JsonException $e) {
55+
+ throw new \Exception('Error decoding body as JSON: '.$e->getMessage());
56+
+ }
57+
+} else {
58+
+ $dataAsString = $body;
59+
+}
60+
```
61+
62+
### post() to requestPost()
63+
64+
This example shows how you can parse the response body of a POST request.
65+
66+
```diff
67+
-// We dont know if we will get xml or a string
68+
-$dataAsXmlOrString = $this->client->post($path, $data);
69+
+$this->client->requestPost($path, $data);
70+
+// $body contains the raw http body of the response
71+
+$body = $this->client->getLastResponseBody();
72+
+
73+
+// if response is XML, create a SimpleXMLElement object
74+
+if ($body !== '' && 0 === strpos($this->client->getLastResponseContentType(), 'application/xml')) {
75+
+ $dataAsXML = new \SimpleXMLElement($body);
76+
+} else {
77+
+ $dataAsString = $body;
78+
+}
79+
```
80+
81+
### put() to requestPut()
82+
83+
This example shows how you can parse the response body of a PUT request.
84+
85+
```diff
86+
-// We dont know if we will get xml or a string
87+
-$dataAsXmlOrString = $this->client->put($path, $data);
88+
+$this->client->requestPut($path, $data);
89+
+// $body contains the raw http body of the response
90+
+$body = $this->client->getLastResponseBody();
91+
+
92+
+// if response is XML, create a SimpleXMLElement object
93+
+if ($body !== '' && 0 === strpos($this->client->getLastResponseContentType(), 'application/xml')) {
94+
+ $dataAsXML = new \SimpleXMLElement($body);
95+
+} else {
96+
+ $dataAsString = $body;
97+
+}
98+
```
99+
100+
### delete() to requestDelete()
101+
102+
This example shows how you can parse the response body of a DELETE request.
103+
104+
```diff
105+
-$dataAsString = $this->client->delete($path);
106+
+$this->client->requestDelte($path);
107+
+$dataAsString = $this->client->getLastResponseBody();
108+
```
109+
110+
### setImpersonateUser() to startImpersonateUser()
111+
112+
If you are using the [Redmine user impersonation](https://www.redmine.org/projects/redmine/wiki/Rest_api#User-Impersonation) you have to change your code.
113+
114+
```diff
115+
// impersonate the user `robin`
116+
-$client->setImpersonateUser('robin');
117+
+$client->startImpersonateUser('robin');
118+
119+
$userData = $client->getApi('user')->getCurrentUser();
120+
121+
// Now stop impersonation
122+
-$client->setImpersonateUser(null);
123+
+$client->stopImpersonateUser();
124+
```
125+
126+
After this changes you should be able to test your code without
127+
128+
## 2. Switch to `Psr18Client`
129+
130+
The `Redmine\Client\Psr18Client` requires:
131+
132+
- a `Psr\Http\Client\ClientInterface` implementation (like guzzlehttp/guzzle), [see packagist.org](https://packagist.org/providers/psr/http-client-implementation)
133+
- a `Psr\Http\Message\ServerRequestFactoryInterface` implementation (like nyholm/psr7), [see packagist.org](https://packagist.org/providers/psr/http-factory-implementation)
134+
- a `Psr\Http\Message\StreamFactoryInterface` implementation (like nyholm/psr7), [see packagist.org](https://packagist.org/providers/psr/http-message-implementation)
135+
- a URL to your Redmine instance
136+
- an Apikey or username
137+
- and optional a password if you want to use username/password (not recommended).
138+
139+
```diff
140+
+$guzzle = new \GuzzleHttp\Client();
141+
+$psr17Factory = new \GuzzleHttp\Psr7\HttpFactory();
142+
+
143+
// Instantiate with ApiKey
144+
-$client = new \Redmine\Client(
145+
+$client = new \Redmine\Client\Prs18Client(
146+
+ $guzzle,
147+
+ $psr17Factory,
148+
+ $psr17Factory,
149+
'https://redmine.example.com',
150+
'1234567890abcdfgh'
151+
);
152+
```
153+
154+
If you want more control over the PSR-17 ServerRequestFactory you can also create a anonymous class:
155+
156+
```diff
157+
+use Psr\Http\Message\ServerRequestFactoryInterface;
158+
+use Psr\Http\Message\ServerRequestInterface;
159+
+use Psr\Http\Message\StreamFactoryInterface;
160+
+use Psr\Http\Message\StreamInterface;
161+
+
162+
$guzzle = new \GuzzleHttp\Client();
163+
-$psr17Factory = new \GuzzleHttp\Psr7\HttpFactory();
164+
+$psr17Factory = new class() implements ServerRequestFactoryInterface, StreamFactoryInterface {
165+
+ public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface
166+
+ {
167+
+ return new \GuzzleHttp\Psr7\ServerRequest($method, $uri);
168+
+ }
169+
+
170+
+ public function createStream(string $content = ''): StreamInterface
171+
+ {
172+
+ return \GuzzleHttp\Psr7\Utils::streamFor($content);
173+
+ }
174+
+
175+
+ public function createStreamFromFile(string $file, string $mode = 'r'): StreamInterface
176+
+ {
177+
+ return \GuzzleHttp\Psr7\Utils::streamFor(\GuzzleHttp\Psr7\Utils::tryFopen($file, $mode));
178+
+ }
179+
+
180+
+ public function createStreamFromResource($resource): StreamInterface
181+
+ {
182+
+ return \GuzzleHttp\Psr7\Utils::streamFor($resource);
183+
+ }
184+
+};
185+
186+
// Instantiate with ApiKey
187+
$client = new \Redmine\Client\Prs18Client(
188+
$guzzle,
189+
$psr17Factory,
190+
$psr17Factory,
191+
'https://redmine.example.com',
192+
'1234567890abcdfgh'
193+
);
194+
```
195+
196+
## 3. Set `cURL` options
197+
198+
If you have set custom `cURL` options you now have to set them to `Guzzle`. Thanks to the HTTP client you can set them to every request:
199+
200+
```diff
201+
$guzzle = new \GuzzleHttp\Client();
202+
$psr17Factory = new \GuzzleHttp\Psr7\HttpFactory();
203+
204+
+$guzzleWrapper = new class(\GuzzleHttp\Client $guzzle) implements \Psr\Http\Client\ClientInterface
205+
+{
206+
+ private $guzzle;
207+
+
208+
+ public function __construct(\GuzzleHttp\Client $guzzle)
209+
+ {
210+
+ $this->guzzle = $guzzle;
211+
+ }
212+
+
213+
+ public function sendRequest(\Psr\Http\Message\RequestInterface $request): \Psr\Http\Message\ResponseInterface
214+
+ {
215+
+ return $this->guzzle->send($request, [
216+
+ // Set other the options for every request here
217+
+ 'auth' => ['username', 'password', 'digest'],
218+
+ 'cert' => ['/path/server.pem', 'password'],
219+
+ 'connect_timeout' => 3.14,
220+
+ // Set specific CURL options, see https://docs.guzzlephp.org/en/stable/faq.html#how-can-i-add-custom-curl-options
221+
+ 'curl' => [
222+
+ CURLOPT_SSL_VERIFYPEER => 1,
223+
+ CURLOPT_SSL_VERIFYHOST => 2,
224+
+ CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2,
225+
+ ],
226+
+ ]);
227+
+ }
228+
+};
229+
+
230+
// Instantiate with ApiKey
231+
$client = new \Redmine\Client\Prs18Client(
232+
- $guzzle,
233+
+ $guzzleWrapper,
234+
$psr17Factory,
235+
$psr17Factory,
236+
'https://redmine.example.com',
237+
'1234567890abcdfgh'
238+
);
239+
-
240+
-$client->setCheckSslCertificate(true);
241+
-$client->setCheckSslHost(true);
242+
-$client->setSslVersion(CURL_SSLVERSION_TLSv1_3);
243+
```
244+
245+
If you don't want `php-redmine-api` to use HTTP auth, you can disable it by removing the headers from the request.
246+
247+
```diff
248+
$guzzle = new \GuzzleHttp\Client();
249+
$psr17Factory = new \GuzzleHttp\Psr7\HttpFactory();
250+
251+
$guzzleWrapper = new class(\GuzzleHttp\Client $guzzle) implements ClientInterface
252+
{
253+
private $guzzle;
254+
255+
public function __construct(\GuzzleHttp\Client $guzzle)
256+
{
257+
$this->guzzle = $guzzle;
258+
}
259+
260+
public function sendRequest(\Psr\Http\Message\RequestInterface $request): \Psr\Http\Message\ResponseInterface
261+
{
262+
+ // Remove the auth headers
263+
+ $request = $request->withoutHeader('X-Redmine-API-Key');
264+
+ $request = $request->withoutHeader('Authorization');
265+
+
266+
return $this->guzzle->send($request, [
267+
// Set other the options for every request here
268+
'auth' => ['username', 'password', 'digest'],
269+
'cert' => ['/path/server.pem', 'password'],
270+
'connect_timeout' => 3.14,
271+
// Set specific CURL options, see https://docs.guzzlephp.org/en/stable/faq.html#how-can-i-add-custom-curl-options
272+
'curl' => [
273+
CURLOPT_SSL_VERIFYPEER => 1,
274+
CURLOPT_SSL_VERIFYHOST => 2,
275+
CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2,
276+
],
277+
]);
278+
}
279+
};
280+
281+
// Instantiate with ApiKey
282+
$client = new \Redmine\Client\Prs18Client(
283+
$guzzleWrapper,
284+
$psr17Factory,
285+
$psr17Factory,
286+
'https://redmine.example.com',
287+
'1234567890abcdfgh'
288+
);
289+
-
290+
-$client->setUseHttpAuth(false);
291+
```
292+
293+
Now you should be ready. Please make sure that you are only using client methods that are defined in `Redmine\Client\Client` because all other methods will be removed or set to private in a future major release. Otherwise you will have to change your code in future again.

0 commit comments

Comments
 (0)