Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Allow `Psr\Http\Message\ServerRequestFactoryInterface` as Argument #2 ($requestFactory) in `Redmine\Client\Psr18Client::__construct()`
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-- Allow `Psr\Http\Message\ServerRequestFactoryInterface` as Argument #2 ($requestFactory) in `Redmine\Client\Psr18Client::__construct()`
+- Allow `Psr\Http\Message\RequestFactoryInterface` as Argument #2 ($requestFactory) in `Redmine\Client\Psr18Client::__construct()`

- Added support for PHP 8.2

### Deprecated

- Providing Argument #2 ($requestFactory) in `Redmine\Client\Psr18Client::__construct()` as type `Psr\Http\Message\ServerRequestFactoryInterface` is deprecated, provide as type `Psr\Http\Message\RequestFactoryInterface` instead
- `Redmine\Api\AbstractApi::attachCustomFieldXML()` is deprecated
- `Redmine\Api\Project::prepareParamsXml()` is deprecated

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ $client = new Redmine\Client\NativeCurlClient('https://redmine.example.com', '12
The `Psr18Client` requires

- a `Psr\Http\Client\ClientInterface` implementation (like guzzlehttp/guzzle), [see](https://packagist.org/providers/psr/http-client-implementation)
- a `Psr\Http\Message\ServerRequestFactoryInterface` implementation (like guzzlehttp/psr7), [see](https://packagist.org/providers/psr/http-factory-implementation)
- a `Psr\Http\Message\RequestFactoryInterface` implementation (like guzzlehttp/psr7), [see](https://packagist.org/providers/psr/http-factory-implementation)
- a `Psr\Http\Message\StreamFactoryInterface` implementation (like guzzlehttp/psr7), [see](https://packagist.org/providers/psr/http-message-implementation)
- a URL to your Redmine instance
- an Apikey or username
Expand Down
14 changes: 7 additions & 7 deletions docs/migrate-to-psr18client.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ After this changes you should be able to test your code without errors.
The `Redmine\Client\Psr18Client` requires:

- a `Psr\Http\Client\ClientInterface` implementation (like guzzlehttp/guzzle), [see packagist.org](https://packagist.org/providers/psr/http-client-implementation)
- a `Psr\Http\Message\ServerRequestFactoryInterface` implementation (like nyholm/psr7), [see packagist.org](https://packagist.org/providers/psr/http-factory-implementation)
- a `Psr\Http\Message\RequestFactoryInterface` implementation (like nyholm/psr7), [see packagist.org](https://packagist.org/providers/psr/http-factory-implementation)
- a `Psr\Http\Message\StreamFactoryInterface` implementation (like nyholm/psr7), [see packagist.org](https://packagist.org/providers/psr/http-message-implementation)
- a URL to your Redmine instance
- an Apikey or username
Expand All @@ -151,20 +151,20 @@ The `Redmine\Client\Psr18Client` requires:
);
```

If you want more control over the PSR-17 ServerRequestFactory you can also create a anonymous class:
If you want more control over the PSR-17 RequestFactory you can also create a anonymous class:

```diff
+use Psr\Http\Message\ServerRequestFactoryInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\RequestFactoryInterface;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\StreamFactoryInterface;
+use Psr\Http\Message\StreamInterface;
+
$guzzle = new \GuzzleHttp\Client();
-$psr17Factory = new \GuzzleHttp\Psr7\HttpFactory();
+$psr17Factory = new class() implements ServerRequestFactoryInterface, StreamFactoryInterface {
+ public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface
+$psr17Factory = new class() implements RequestFactoryInterface, StreamFactoryInterface {
+ public function createRequest(string $method, $uri): RequestInterface
+ {
+ return new \GuzzleHttp\Psr7\ServerRequest($method, $uri);
+ return new \GuzzleHttp\Psr7\Request($method, $uri);
+ }
+
+ public function createStream(string $content = ''): StreamInterface
Expand Down
2 changes: 1 addition & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ $client = new \Redmine\Client('https://redmine.example.com', '1234567890abcdfgh'
The `Psr18Client` requires

- a `Psr\Http\Client\ClientInterface` implementation (like guzzlehttp/guzzle) ([possible implementations](https://packagist.org/providers/psr/http-client-implementation))
- a `Psr\Http\Message\ServerRequestFactoryInterface` implementation (like nyholm/psr7) ([possible implementations](https://packagist.org/providers/psr/http-factory-implementation))
- a `Psr\Http\Message\RequestFactoryInterface` implementation (like nyholm/psr7) ([possible implementations](https://packagist.org/providers/psr/http-factory-implementation))
- a `Psr\Http\Message\StreamFactoryInterface` implementation (like nyholm/psr7) ([possible implementations](https://packagist.org/providers/psr/http-message-implementation))
- a URL to your Redmine instance
- an Apikey or username
Expand Down
60 changes: 53 additions & 7 deletions src/Redmine/Client/Psr18Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@

namespace Redmine\Client;

use Exception;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Redmine\Exception\ClientException;

Expand All @@ -24,22 +26,45 @@ final class Psr18Client implements Client
private ?string $password;
private ?string $impersonateUser = null;
private ClientInterface $httpClient;
private ServerRequestFactoryInterface $requestFactory;
private RequestFactoryInterface $requestFactory;
private StreamFactoryInterface $streamFactory;
private ?ResponseInterface $lastResponse = null;

/**
* $apikeyOrUsername should be your ApiKey, but it could also be your username.
* $password needs to be set if a username is given (not recommended).
* @param RequestFactoryInterface|ServerRequestFactoryInterface $requestFactory
* @param string $apikeyOrUsername should be your ApiKey, but it could also be your username.
* @param ?string $password needs to be set if a username is given (not recommended).
*/
public function __construct(
ClientInterface $httpClient,
ServerRequestFactoryInterface $requestFactory,
$requestFactory,
StreamFactoryInterface $streamFactory,
string $url,
string $apikeyOrUsername,
string $password = null
) {
if ($requestFactory instanceof ServerRequestFactoryInterface) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also check if $requestFactory is not an instance of RequestFactoryInterface already. \GuzzleHttp\Psr7\HttpFactory from the README example implements both interfaces.

@trigger_error(
sprintf(
'%s(): Providing Argument #2 ($requestFactory) as %s is deprecated since v2.3.0, please provide as %s instead.',
__METHOD__,
ServerRequestFactoryInterface::class,
RequestFactoryInterface::class
),
E_USER_DEPRECATED
);

$requestFactory = $this->handleServerRequestFactory($requestFactory);
}

if (! $requestFactory instanceof RequestFactoryInterface) {
throw new Exception(sprintf(
'%s(): Argument #2 ($requestFactory) must be of type %s',
__METHOD__,
RequestFactoryInterface::class
));
}

$this->httpClient = $httpClient;
$this->requestFactory = $requestFactory;
$this->streamFactory = $streamFactory;
Expand Down Expand Up @@ -155,9 +180,9 @@ private function runRequest(string $method, string $path, string $body = ''): bo
return $this->lastResponse->getStatusCode() < 400;
}

private function createRequest(string $method, string $path, string $body = ''): ServerRequestInterface
private function createRequest(string $method, string $path, string $body = ''): RequestInterface
{
$request = $this->requestFactory->createServerRequest(
$request = $this->requestFactory->createRequest(
$method,
$this->url.$path
);
Expand Down Expand Up @@ -215,4 +240,25 @@ private function createRequest(string $method, string $path, string $body = ''):

return $request;
}

/**
* We accept ServerRequestFactoryInterface for BC
*/
private function handleServerRequestFactory(ServerRequestFactoryInterface $factory): RequestFactoryInterface
{
return new class($factory) implements RequestFactoryInterface
{
private ServerRequestFactoryInterface $factory;

public function __construct(ServerRequestFactoryInterface $factory)
{
$this->factory = $factory;
}

public function createRequest(string $method, $uri): RequestInterface
{
return $this->factory->createServerRequest($method, $uri);
}
};
}
}
20 changes: 5 additions & 15 deletions tests/Integration/Psr18ClientRequestGenerationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

namespace Redmine\Tests\Integration;

use GuzzleHttp\Psr7\ServerRequest;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Utils;
use PHPUnit\Framework\TestCase;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\StreamInterface;
use Redmine\Client\Psr18Client;
Expand Down Expand Up @@ -41,16 +41,6 @@ public function testPsr18ClientCreatesCorrectRequests(
$headers .= $k.': '.$request->getHeaderLine($k).\PHP_EOL;
}

$cookies = [];

foreach ($request->getCookieParams() as $k => $v) {
$cookies[] = $k.'='.$v;
}

if (!empty($cookies)) {
$headers .= 'Cookie: '.implode('; ', $cookies).\PHP_EOL;
}

$fullRequest = sprintf(
'%s %s HTTP/%s',
$request->getMethod(),
Expand All @@ -67,10 +57,10 @@ public function testPsr18ClientCreatesCorrectRequests(
})
);

$requestFactory = $this->createMock(ServerRequestFactoryInterface::class);
$requestFactory->method('createServerRequest')->will(
$requestFactory = $this->createMock(RequestFactoryInterface::class);
$requestFactory->method('createRequest')->will(
$this->returnCallback(function ($method, $uri) {
return new ServerRequest($method, $uri);
return new Request($method, $uri);
})
);

Expand Down
53 changes: 36 additions & 17 deletions tests/Unit/Client/Psr18ClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\StreamInterface;
use Redmine\Client\Client;
Expand All @@ -20,6 +21,24 @@ class Psr18ClientTest extends TestCase
* @test
*/
public function shouldPassApiKeyToConstructor()
{
$client = new Psr18Client(
$this->createMock(ClientInterface::class),
$this->createMock(RequestFactoryInterface::class),
$this->createMock(StreamFactoryInterface::class),
'http://test.local',
'access_token'
);

$this->assertInstanceOf(Psr18Client::class, $client);
$this->assertInstanceOf(Client::class, $client);
}

/**
* @covers \Redmine\Client\Psr18Client
* @test
*/
public function acceptServerRequestFactoryInConstructorForBC()
{
$client = new Psr18Client(
$this->createMock(ClientInterface::class),
Expand All @@ -41,7 +60,7 @@ public function shouldPassUsernameAndPasswordToConstructor()
{
$client = new Psr18Client(
$this->createMock(ClientInterface::class),
$this->createMock(ServerRequestFactoryInterface::class),
$this->createMock(RequestFactoryInterface::class),
$this->createMock(StreamFactoryInterface::class),
'http://test.local',
'username',
Expand All @@ -56,11 +75,11 @@ public function shouldPassUsernameAndPasswordToConstructor()
* @covers \Redmine\Client\Psr18Client
* @test
*/
public function testGetLastResponseStatusCodeIsInitialNull()
public function testGetLastResponseStatusCodeIsInitialZero()
{
$client = new Psr18Client(
$this->createMock(ClientInterface::class),
$this->createMock(ServerRequestFactoryInterface::class),
$this->createMock(RequestFactoryInterface::class),
$this->createMock(StreamFactoryInterface::class),
'http://test.local',
'access_token'
Expand All @@ -77,7 +96,7 @@ public function testGetLastResponseContentTypeIsInitialEmpty()
{
$client = new Psr18Client(
$this->createMock(ClientInterface::class),
$this->createMock(ServerRequestFactoryInterface::class),
$this->createMock(RequestFactoryInterface::class),
$this->createMock(StreamFactoryInterface::class),
'http://test.local',
'access_token'
Expand All @@ -94,7 +113,7 @@ public function testGetLastResponseBodyIsInitialEmpty()
{
$client = new Psr18Client(
$this->createMock(ClientInterface::class),
$this->createMock(ServerRequestFactoryInterface::class),
$this->createMock(RequestFactoryInterface::class),
$this->createMock(StreamFactoryInterface::class),
'http://test.local',
'access_token'
Expand All @@ -109,7 +128,7 @@ public function testGetLastResponseBodyIsInitialEmpty()
*/
public function testStartAndStopImpersonateUser()
{
$request = $this->createMock(ServerRequestInterface::class);
$request = $this->createMock(RequestInterface::class);
$request->expects($this->exactly(4))
->method('withHeader')
->willReturnMap([
Expand All @@ -119,8 +138,8 @@ public function testStartAndStopImpersonateUser()
['X-Redmine-API-Key', 'access_token', $request],
]);

$requestFactory = $this->createMock(ServerRequestFactoryInterface::class);
$requestFactory->method('createServerRequest')->willReturn($request);
$requestFactory = $this->createMock(RequestFactoryInterface::class);
$requestFactory->method('createRequest')->willReturn($request);

$client = new Psr18Client(
$this->createMock(ClientInterface::class),
Expand Down Expand Up @@ -149,11 +168,11 @@ public function testRequestGetReturnsFalse()
$httpClient = $this->createMock(ClientInterface::class);
$httpClient->method('sendRequest')->willReturn($response);

$request = $this->createMock(ServerRequestInterface::class);
$request = $this->createMock(RequestInterface::class);
$request->method('withHeader')->willReturn($request);

$requestFactory = $this->createMock(ServerRequestFactoryInterface::class);
$requestFactory->method('createServerRequest')->willReturn($request);
$requestFactory = $this->createMock(RequestFactoryInterface::class);
$requestFactory->method('createRequest')->willReturn($request);

$client = new Psr18Client(
$httpClient,
Expand Down Expand Up @@ -184,12 +203,12 @@ public function testRequestsReturnsCorrectContent($method, $data, $boolReturn, $
$httpClient = $this->createMock(ClientInterface::class);
$httpClient->method('sendRequest')->willReturn($response);

$request = $this->createMock(ServerRequestInterface::class);
$request = $this->createMock(RequestInterface::class);
$request->method('withHeader')->willReturn($request);
$request->method('withBody')->willReturn($request);

$requestFactory = $this->createMock(ServerRequestFactoryInterface::class);
$requestFactory->method('createServerRequest')->willReturn($request);
$requestFactory = $this->createMock(RequestFactoryInterface::class);
$requestFactory->method('createRequest')->willReturn($request);

$client = new Psr18Client(
$httpClient,
Expand Down Expand Up @@ -243,7 +262,7 @@ public function getApiShouldReturnApiInstance($apiName, $class)
{
$client = new Psr18Client(
$this->createMock(ClientInterface::class),
$this->createMock(ServerRequestFactoryInterface::class),
$this->createMock(RequestFactoryInterface::class),
$this->createMock(StreamFactoryInterface::class),
'http://test.local',
'access_token'
Expand Down Expand Up @@ -285,7 +304,7 @@ public function getApiShouldThrowException()
{
$client = new Psr18Client(
$this->createMock(ClientInterface::class),
$this->createMock(ServerRequestFactoryInterface::class),
$this->createMock(RequestFactoryInterface::class),
$this->createMock(StreamFactoryInterface::class),
'http://test.local',
'access_token'
Expand Down