Skip to content

Commit 238cef9

Browse files
authored
Introduce new PSR-18 based client (#257) @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 * Move usage docs into README.md
1 parent e534a50 commit 238cef9

Some content is hidden

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

46 files changed

+3372
-1242
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.php_cs.cache
2+
.phpunit.result.cache
23
composer.lock
34
composer.phar
45
coverage.clover

README.md

Lines changed: 190 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Uses [Redmine API](http://www.redmine.org/projects/redmine/wiki/Rest_api/).
66

77
## Features
88

9-
* Follows PSR-0 conventions and coding standard: autoload friendly
9+
* Follows PSR-4 conventions and coding standard: autoload friendly
1010
* API entry points implementation state :
1111
* OK Attachments
1212
* OK Groups
@@ -45,14 +45,14 @@ A possible solution to this would be to create an extra APIs implementing the mi
4545

4646
## Requirements
4747

48-
* PHP >= 5.4
48+
* PHP ^7.3 || ^8.0
4949
* The PHP [cURL](http://php.net/manual/en/book.curl.php) extension
5050
* The PHP [SimpleXML](http://php.net/manual/en/book.simplexml.php) extension
5151
* The PHP [JSON](http://php.net/manual/en/book.json.php) extension
52-
* [PHPUnit](https://phpunit.de/) >= 4.0 (optional) to run the test suite
52+
* [PHPUnit](https://phpunit.de/) >= 9.0 (optional) to run the test suite
5353
* "Enable REST web service" for your Redmine project (/settings/edit?tab=authentication)
5454
* then obtain your *API access key* in your profile page : /my/account
55-
* or use your *username & password*
55+
* or use your *username & password* (not recommended)
5656

5757
## Install
5858

@@ -61,7 +61,7 @@ A possible solution to this would be to create an extra APIs implementing the mi
6161
[Composer](http://getcomposer.org/download/) users can simply run:
6262

6363
```bash
64-
$ php composer.phar require kbsali/redmine-api:~1.0
64+
$ php composer.phar require kbsali/redmine-api
6565
```
6666

6767
at the root of their projects. To utilize the library, include
@@ -74,6 +74,7 @@ For example,
7474
<?php
7575
// This file is generated by Composer
7676
require_once 'vendor/autoload.php';
77+
7778
$client = new Redmine\Client('http://redmine.example.com', 'username', 'password');
7879
```
7980

@@ -82,25 +83,23 @@ $client = new Redmine\Client('http://redmine.example.com', 'username', 'password
8283
It is also possible to install the library oneself, either locally to
8384
a project or globally; say, in `/usr/share/php`.
8485

85-
First, download and extract the library somewhere. For example, the
86-
following steps extract v1.5.18 of the library into the
87-
`vendor/php-redmine-api-1.5.18` directory:
86+
Download the library from [php-download.com](https://php-download.com/package/kbsali/redmine-api). The advantage of using this site is that no Composer installation is required. This service will resolve all composer dependencies for you and create a zip archive with `vendor/autoload.php` for you.
87+
88+
Than extract the library somewhere. For example, the following steps extract v1.6.0 of the library into the `vendor/php-redmine-api-1.6.0` directory:
8889

8990
```bash
90-
$ mkdir vendor
91-
$ wget -q https://github.com/kbsali/php-redmine-api/archive/v1.5.18.tar.gz
92-
$ tar -xf v1.5.18.tar.gz -C vendor/
93-
$ rm v1.5.18.tar.gz
91+
$ unzip kbsali_redmine_api_1.6.0.0_require.zip
92+
$ rm kbsali_redmine_api_1.6.0.0_require.zip
9493
```
9594

96-
Now, in any scripts that will use the `Redmine` classes, include the
97-
`src/autoload.php` file from the php-redmine-api directory. For
95+
Now, in any scripts that will use the `Redmine` classes, include the `vendor/autoload.php` file from the php-redmine-api directory. For
9896
example,
9997

10098
```php
10199
<?php
102100
// This file ships with php-redmine-api
103-
require 'vendor/php-redmine-api-1.5.18/src/autoload.php';
101+
require 'vendor/php-redmine-api-1.6.0/vendor/autoload.php';
102+
104103
$client = new Redmine\Client('http://redmine.example.com', 'username', 'password');
105104
```
106105

@@ -110,51 +109,203 @@ You can run test suite to make sure the library will work properly on your syste
110109

111110
```
112111
$ vendor/bin/phpunit
113-
PHPUnit 6.5.14 by Sebastian Bergmann and contributors.
112+
PHPUnit 9.5.3 by Sebastian Bergmann and contributors.
114113
115-
Error: No code coverage driver is available
114+
Warning: No code coverage driver available
116115
117-
............................................................... 63 / 285 ( 22%)
118-
............................................................... 126 / 285 ( 44%)
119-
............................................................... 189 / 285 ( 66%)
120-
............................................................... 252 / 285 ( 88%)
121-
................................. 285 / 285 (100%)
116+
............................................................... 63 / 445 ( 14%)
117+
............................................................... 126 / 445 ( 28%)
118+
............................................................... 189 / 445 ( 42%)
119+
............................................................... 252 / 445 ( 56%)
120+
............................................................... 315 / 445 ( 70%)
121+
............................................................... 378 / 445 ( 84%)
122+
............................................................... 441 / 445 ( 99%)
123+
.... 445 / 445 (100%)
122124
123-
Time: 107 ms, Memory: 8.00MB
125+
Time: 00:00.102, Memory: 12.00 MB
124126
125-
OK (285 tests, 662 assertions)
127+
OK (445 tests, 993 assertions)
126128
```
127129

128130
## Basic usage of `php-redmine-api` client
129131

130-
```php
132+
### Start your project
133+
134+
Create your project e.g. in the `index.php` by require the `vendor/autoload.php` file.
135+
136+
```diff
137+
+<?php
138+
+
139+
+require_once 'vendor/autoload.php';
140+
```
141+
142+
### Instantiate a Redmine Client
143+
144+
You can choose between the navite curl client or the PSR-18 compatible client.
145+
146+
#### Native curl Client `Redmine\Client`
147+
148+
Every Client requires a URL to your Redmine instance and either a valid Apikey...
149+
150+
```diff
131151
<?php
132152

133-
// For Composer users (this file is generated by Composer)
134153
require_once 'vendor/autoload.php';
154+
+
155+
+// Instantiate with ApiKey
156+
+$client = new Redmine\Client('http://localhost', '1234567890abcdfgh');
157+
```
135158

136-
// Or if you've installed the library manually, use this instead.
137-
// require 'vendor/php-redmine-api-x.y.z/src/autoload.php';
159+
... or valid username/password.
138160

139-
$client = new Redmine\Client('http://redmine.example.com', 'API_ACCESS_KEY');
140-
//-- OR --
141-
$client = new Redmine\Client('http://redmine.example.com', 'username', 'password');
142161

143-
$client->user->all();
144-
$client->user->listing();
162+
```diff
163+
<?php
164+
165+
require_once 'vendor/autoload.php';
166+
+
167+
+// Instantiate with Username/Password (not recommended)
168+
+$client = new Redmine\Client('http://redmine.example.com', 'username', 'password');
169+
```
170+
171+
> :bulb: For security reason it is recommended that you use an ApiKey rather than your username/password.
172+
173+
After you instantiate a client you can set some optional settings.
174+
175+
```diff
176+
<?php
177+
178+
require_once 'vendor/autoload.php';
179+
180+
// Instantiate with ApiKey
181+
$client = new Redmine\Client('https://redmine.example.com', '1234567890abcdfgh');
182+
+
183+
+// [OPTIONAL] if you want to check the servers' SSL certificate on Curl call
184+
+$client->setCheckSslCertificate(true);
185+
+
186+
+// [OPTIONAL] set the port (it will try to guess it from the url)
187+
+$client->setPort(8080);
188+
+
189+
+// [OPTIONAL] set a custom host
190+
+$client->setCustomHost('https://localhost:8080');
191+
192+
```
193+
#### Psr-18 compatible Client `Redmine\Client\Psr18Client`
194+
195+
The `Psr18Client` requires
196+
197+
- a `Psr\Http\Client\ClientInterface` implementation (like guzzlehttp/guzzle), [see](https://packagist.org/providers/psr/http-client-implementation)
198+
- a `Psr\Http\Message\ServerRequestFactoryInterface` implementation (like nyholm/psr7), [see](https://packagist.org/providers/psr/http-factory-implementation)
199+
- a `Psr\Http\Message\StreamFactoryInterface` implementation (like nyholm/psr7), [see](https://packagist.org/providers/psr/http-message-implementation)
200+
- a URL to your Redmine instance
201+
- an Apikey or username
202+
- and optional a password if you want tu use username/password.
203+
204+
> :bulb: For security reason it is recommended that you use an ApiKey rather than your username/password.
205+
206+
```diff
207+
<?php
208+
209+
require_once 'vendor/autoload.php';
210+
+
211+
+$guzzle = \GuzzleHttp\Client();
212+
+$psr17Factory = new \Nyholm\Psr7\Factory\Psr17Factory();
213+
+
214+
+// Instantiate with ApiKey
215+
+$client = new Redmine\Client\Prs18Client($guzzle, $psr17Factory, $psr17Factory, 'https://redmine.example.com', '1234567890abcdfgh');
216+
+// ...or Instantiate with Username/Password (not recommended)
217+
+$client = new Redmine\Client\Prs18Client($guzzle, $psr17Factory, $psr17Factory, 'https://redmine.example.com', 'username', 'password');
218+
```
219+
220+
##### Guzzle configuration
221+
222+
Because the `Psr18Client` is agnostic about the HTTP client implementation every configuration specific to the transport has to be set to the `Psr\Http\Client\ClientInterface` implementation.
223+
224+
This means that if you want to set any `cURL` settings to `Guzzle` you have multiple ways to set them:
225+
226+
1. Using [Guzzle environment variables](https://docs.guzzlephp.org/en/stable/quickstart.html#environment-variables)
227+
2. Using [request options](https://docs.guzzlephp.org/en/stable/request-options.html) inside a `Psr\Http\Client\ClientInterface` wrapper:
228+
229+
```diff
230+
<?php
231+
232+
require_once 'vendor/autoload.php';
233+
234+
+use Psr\Http\Client\ClientInterface;
235+
+use Psr\Http\Message\RequestInterface;
236+
+use Psr\Http\Message\ResponseInterface;
237+
+
238+
$guzzle = \GuzzleHttp\Client();
239+
$psr17Factory = new \Nyholm\Psr7\Factory\Psr17Factory();
240+
241+
+$guzzleWrapper = new class(\GuzzleHttp\Client $guzzle) implements ClientInterface
242+
+{
243+
+ private $guzzle;
244+
+
245+
+ public function __construct(\GuzzleHttp\Client $guzzle)
246+
+ {
247+
+ $this->guzzle = $guzzle;
248+
+ }
249+
+
250+
+ public function sendRequest(RequestInterface $request): ResponseInterface
251+
+ {
252+
+ return $this->guzzle->send($request, [
253+
+ // Set the options for every request here
254+
+ 'auth' => ['username', 'password', 'digest'],
255+
+ 'cert' => ['/path/server.pem', 'password'],
256+
+ 'connect_timeout' => 3.14,
257+
+ // Set specific CURL options, see https://docs.guzzlephp.org/en/stable/faq.html#how-can-i-add-custom-curl-options
258+
+ 'curl' => [
259+
+ CURLOPT_SSL_VERIFYPEER => 1,
260+
+ CURLOPT_SSL_VERIFYHOST => 2,
261+
+ CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2,
262+
+ ],
263+
+ ]);
264+
+ }
265+
+};
266+
+
267+
// Instantiate with ApiKey
268+
-$client = new Redmine\Client\Prs18Client($guzzle, $psr17Factory, $psr17Factory, 'https://redmine.example.com', '1234567890abcdfgh');
269+
+$client = new Redmine\Client\Prs18Client($guzzleWrapper, $psr17Factory, $psr17Factory, 'https://redmine.example.com', '1234567890abcdfgh');
270+
271+
```
272+
273+
## Built-in Redmine features
274+
275+
### Impersonate User
276+
277+
Redmine allows you [to impersonate another user](https://www.redmine.org/projects/redmine/wiki/Rest_api#User-Impersonation). This can be done using the methods `startImpersonateUser()` and `stopImpersonateUser()`.
278+
279+
```php
280+
$client->startImpersonateUser('kim');
281+
// all requests will now impersonate the user `kim`
282+
283+
// To stop impersonation
284+
$client->stopImpersonateUser();
285+
```
286+
287+
### API usage
288+
289+
You can now use the `getApi()` method to create and get a specific Redmine API.
290+
291+
```php
292+
<?php
293+
294+
$client->getApi('user')->all();
295+
$client->getApi('user')->listing();
145296

146-
$client->issue->create([
297+
$client->getApi('issue')->create([
147298
'project_id' => 'test',
148299
'subject' => 'some subject',
149300
'description' => 'a long description blablabla',
150301
'assigned_to_id' => 123, // or 'assigned_to' => 'user1'
151302
]);
152-
$client->issue->all([
303+
$client->getApi('issue')->all([
153304
'limit' => 1000
154305
]);
155306
```
156307

157-
See `example.php` for further examples.
308+
See `[example.php](example.php)` for further examples.
158309

159310
## User Impersonation
160311

@@ -165,13 +316,13 @@ As of Redmine V2.2 you can impersonate user through the REST API :
165316
$client = new Redmine\Client('http://redmine.example.com', 'API_ACCESS_KEY');
166317

167318
// impersonate user
168-
$client->setImpersonateUser('jsmith');
319+
$client->startImpersonateUser('jsmith');
169320

170321
// create a time entry for jsmith
171-
$client->time_entry->create($data);
322+
$client->getApi('time_entry')->create($data);
172323

173324
// remove impersonation for further calls
174-
$client->setImpersonateUser(null);
325+
$client->stopImpersonateUser();
175326
```
176327

177328

composer.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@
1919
"php": "^7.3 || ^8.0",
2020
"ext-curl": "*",
2121
"ext-simplexml": "*",
22-
"ext-json": "*"
22+
"ext-json": "*",
23+
"psr/http-client": "^1.0",
24+
"psr/http-factory": "^1.0"
2325
},
2426
"require-dev": {
2527
"friendsofphp/php-cs-fixer": "^2.0",
26-
"phpunit/phpunit": "^9.5"
28+
"phpunit/phpunit": "^9.5",
29+
"guzzlehttp/psr7": "^1.7"
2730
},
2831
"autoload": {
2932
"psr-4": {

0 commit comments

Comments
 (0)