Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
11 changes: 1 addition & 10 deletions .github/workflows/phpunit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,14 @@ jobs:
dependencies:
- "highest"
php-version:
- "7.2"
- "7.3"
- "7.4"
- "8.0"
- "8.1"
- "8.2"
- "8.3"
- "8.4"
- "8.5"

include:
- php-version: '7.2'
- php-version: '8.1'
dependencies: "lowest"

steps:
Expand All @@ -51,10 +47,5 @@ jobs:
if: ${{ matrix.dependencies == 'highest' }}
run: "composer update --no-interaction --no-progress"

- name: "Tests (PHPUnit 9)"
if: ${{ matrix.php-version <= '8.0' }}
run: "vendor/bin/phpunit --configuration phpunit9.xml.dist"

- name: "Tests (PHPUnit 10+)"
if: ${{ matrix.php-version >= '8.1' }}
run: "vendor/bin/phpunit"
11 changes: 6 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,21 @@
"classmap": ["tests/OmnipayTest.php"]
},
"require": {
"php": "^7.2|^8",
"php": "^8",
"php-http/client-implementation": "^1",
Comment on lines +45 to 46

Copilot AI Jan 29, 2026

Copy link

Choose a reason for hiding this comment

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

The PR description suggests this is not a big BC break, but composer.json now requires PHP ^8 (dropping ^7.2|^8). If this is intentional, it should be called out explicitly as a BC break (and aligned with the project’s release/UPGRADE notes). If not intentional, restore the previous PHP constraint.

Copilot uses AI. Check for mistakes.
"php-http/message": "^1.5",
"php-http/message-factory": "^1.1",
"php-http/discovery": "^1.14",
"symfony/http-foundation": "^2.1|^3|^4|^5|^6|^7|^8",
"moneyphp/money": "^3.1|^4.0.3"
"symfony/http-foundation": "^5.4.50|^6|^7|^8",
"moneyphp/money": "^3.1|^4.0.3",
"psr/http-factory": "^1"
},
"require-dev": {
"omnipay/tests": "^4.1",
"php-http/mock-client": "^1.6",
"php-http/guzzle7-adapter": "^1",
"squizlabs/php_codesniffer": "^3.8.1",
"http-interop/http-factory-guzzle": "^1.1"
"http-interop/http-factory-guzzle": "^1.1",
"php-http/message-factory": "^1.1"
},
Comment on lines 53 to 60

Copilot AI Jan 29, 2026

Copy link

Choose a reason for hiding this comment

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

composer.json moves php-http/message-factory to require-dev, but production code (e.g., src/Common/Http/AbstractClient.php and src/Common/Http/Client.php) still references Http\Message\RequestFactory in imports and type declarations. If users install without require-dev, PHP will fatal when loading these classes. Either keep php-http/message-factory in require, or remove the hard type dependency (e.g., accept an untyped legacy factory and only interact with it behind an interface_exists/class_exists check).

Copilot uses AI. Check for mistakes.
"extra": {
"branch-alias": {
Expand Down
53 changes: 41 additions & 12 deletions src/Common/Http/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,54 @@

namespace Omnipay\Common\Http;

use Http\Client\HttpClient;
use Http\Discovery\HttpClientDiscovery;
use Http\Discovery\MessageFactoryDiscovery;
use Http\Discovery\Psr17FactoryDiscovery;
use Http\Discovery\Psr18ClientDiscovery;
use Http\Message\RequestFactory;
use Omnipay\Common\Http\Exception\NetworkException;
use Omnipay\Common\Http\Exception\RequestException;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\StreamInterface;

class Client implements ClientInterface
{
/**
* The Http Client which implements `public function sendRequest(RequestInterface $request)`
* Note: Will be changed to PSR-18 when released
*
* @var HttpClient
* @var \Psr\Http\Client\ClientInterface|\Http\Client\HttpClient
*/
private $httpClient;

/**
* @var RequestFactory
* @var RequestFactoryInterface|RequestFactory
*/
private $requestFactory;

public function __construct($httpClient = null, ?RequestFactory $requestFactory = null)
{
$this->httpClient = $httpClient ?: HttpClientDiscovery::find();
$this->requestFactory = $requestFactory ?: MessageFactoryDiscovery::find();
/**
* @var StreamFactoryInterface
*/
private $streamFactory;

/**
* @param \Psr\Http\Client\ClientInterface|\Http\Client\HttpClient|null $httpClient
*/
public function __construct(
$httpClient = null,
null|RequestFactoryInterface|RequestFactory $requestFactory = null,
null|StreamFactoryInterface $streamFactory = null
) {
$this->httpClient = $httpClient ?: Psr18ClientDiscovery::find();
$this->requestFactory = $requestFactory ?: Psr17FactoryDiscovery::findRequestFactory();
$this->streamFactory = $streamFactory ?: Psr17FactoryDiscovery::findStreamFactory();
}

/**
* @param $method
* @param $uri
* @param array $headers
* @param string|array|resource|StreamInterface|null $body
Comment thread
barryvdh marked this conversation as resolved.
* @param string|resource|StreamInterface|null $body
* @param string $protocolVersion
* @return ResponseInterface
* @throws \Http\Client\Exception
Expand All @@ -49,7 +61,24 @@ public function request(
$body = null,
$protocolVersion = '1.1'
) {
$request = $this->requestFactory->createRequest($method, $uri, $headers, $body, $protocolVersion);
$request = $this->requestFactory->createRequest($method, $uri)->withProtocolVersion($protocolVersion);

foreach ($headers as $name => $value) {
$request = $request->withHeader($name, $value);
}

if (null !== $body) {
if (is_resource($body)) {
$stream = $this->streamFactory->createStreamFromResource($body);
} elseif (is_string($body)) {
$stream = $this->streamFactory->createStream($body);
} elseif ($body instanceof StreamInterface) {
$stream = $body;
} else {
throw new \InvalidArgumentException('Invalid body type.');
}
$request = $request->withBody($stream);
}

return $this->sendRequest($request);
}
Expand Down
119 changes: 105 additions & 14 deletions tests/Common/Http/ClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@

use GuzzleHttp\Psr7\Response;
use Http\Client\Exception\NetworkException;
use Http\Discovery\MessageFactoryDiscovery;
use Http\Discovery\Psr17FactoryDiscovery;
use Http\Factory\Guzzle\StreamFactory;
use Mockery as m;
use GuzzleHttp\Psr7\Request;
use Http\Client\HttpClient;
use Http\Message\RequestFactory;
use Omnipay\Common\Http\Exception\RequestException;
use Omnipay\Tests\TestCase;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamFactoryInterface;

class ClientTest extends TestCase
{
Expand All @@ -26,9 +32,6 @@ public function testSend()
$mockFactory->shouldReceive('createRequest')->withArgs([
'GET',
'/path',
[],
null,
'1.1',
])->andReturn($request);

$mockClient->shouldReceive('sendRequest')
Expand All @@ -37,13 +40,110 @@ public function testSend()
->once();

$this->assertSame($response, $client->request('GET', '/path'));
}

public function testSendRequest()
{
$mockClient = m::mock(HttpClient::class);

$client = new Client($mockClient, MessageFactoryDiscovery::find());

$response = new Response();

$mockClient->shouldReceive('sendRequest')
->withArgs(function (RequestInterface $request) {
return $request->getMethod() === 'GET'
&& $request->getUri()->getPath() === '/path'
&& $request->getHeader('Content-Type')[0] === 'application/json'
&& $request->getBody()->getContents() === '{"a":"b"}'
;
})
->andReturn($response)
->once();

$this->assertSame($response, $client->request('GET', '/path', [
'Content-Type' => 'application/json'
], json_encode(['a' => 'b'])));
}

public function testSendPsr()
{
$mockClient = m::mock(\Psr\Http\Client\ClientInterface::class);
$mockFactory = m::mock(RequestFactoryInterface::class);
$client = new Client($mockClient, $mockFactory);

$request = new Request('GET', '/path');
$response = new Response();

$mockFactory->shouldReceive('createRequest')->withArgs([
'GET',
'/path',
])->andReturn($request);

$mockClient->shouldReceive('sendRequest')
->andReturn($response)
->once();

$this->assertSame($response, $client->request('GET', '/path'));
}

public function testSendRequestPsr()
{
$mockClient = m::mock(\Psr\Http\Client\ClientInterface::class);

$client = new Client($mockClient, Psr17FactoryDiscovery::findRequestFactory());

$response = new Response();

$mockClient->shouldReceive('sendRequest')
->withArgs(function (RequestInterface $request) {
return $request->getMethod() === 'GET'
&& $request->getUri()->getPath() === '/path'
&& $request->getHeader('Content-Type')[0] === 'application/json'
&& $request->getBody()->getContents() === '{"a":"b"}'
;
})
->andReturn($response)
->once();

$this->assertSame($response, $client->request('GET', '/path', [
'Content-Type' => 'application/json'
], json_encode(['a' => 'b'])));
}

public function testSendRequestBody()
{
$mockClient = m::mock(\Psr\Http\Client\ClientInterface::class);
$mockStream = m::mock(StreamFactoryInterface::class);

$client = new Client($mockClient, Psr17FactoryDiscovery::findRequestFactory(), $mockStream);

$request = new Request('GET', '/path');
$response = new Response();

$streamFactory = new StreamFactory();
$stream = $streamFactory->createStream('{"a":"b"}');

$mockStream->shouldReceive('createStream')
->withArgs(['{"a":"b"}'])
->andReturn($stream);

$mockClient->shouldReceive('sendRequest')
->withArgs(function (RequestInterface $request) use ($stream) {
return $request->getBody() === $stream;
})
->andReturn($response)
->once();

$this->assertSame($response, $client->request('GET', '/path', [
'Content-Type' => 'application/json'
], json_encode(['a' => 'b'])));
}

public function testSendException()
{
$mockClient = m::mock(HttpClient::class);
$mockFactory = m::mock(RequestFactory::class);
$mockClient = m::mock(\Psr\Http\Client\ClientInterface::class);
$mockFactory = m::mock(RequestFactoryInterface::class);
$client = new Client($mockClient, $mockFactory);

$request = new Request('GET', '/path');
Expand All @@ -52,9 +152,6 @@ public function testSendException()
$mockFactory->shouldReceive('createRequest')->withArgs([
'GET',
'/path',
[],
null,
'1.1',
])->andReturn($request);

$mockClient->shouldReceive('sendRequest')
Expand All @@ -79,9 +176,6 @@ public function testSendNetworkException()
$mockFactory->shouldReceive('createRequest')->withArgs([
'GET',
'/path',
[],
null,
'1.1',
])->andReturn($request);

$mockClient->shouldReceive('sendRequest')
Expand All @@ -106,9 +200,6 @@ public function testSendExceptionGetRequest()
$mockFactory->shouldReceive('createRequest')->withArgs([
'GET',
'/path',
[],
null,
'1.1',
])->andReturn($request);

$exception = new \Exception('Something went wrong');
Expand Down