diff --git a/README.md b/README.md
index 05754f3..b2f7a9f 100644
--- a/README.md
+++ b/README.md
@@ -6,13 +6,14 @@
Openapi® client for PHP
The perfect starting point to integrate Openapi® within your PHP project
- [](https://github.com/openapi/openapi-php-sdk/actions/workflows/php.yml)
- [](https://packagist.org/packages/openapi/openapi-sdk)
- [](https://packagist.org/packages/openapi/openapi-sdk)
- [](LICENSE)
- [](https://packagist.org/packages/openapi/openapi-sdk)
-
+[](https://github.com/openapi/openapi-php-sdk/actions/workflows/php.yml)
+[](https://packagist.org/packages/openapi/openapi-sdk)
+[](https://packagist.org/packages/openapi/openapi-sdk)
+[](LICENSE)
+[](https://packagist.org/packages/openapi/openapi-sdk)
+
[](https://www.linuxfoundation.org/about/members)
+
## Overview
@@ -27,7 +28,7 @@ Before using the Openapi PHP Client, you will need an account at [Openapi](https
- **Agnostic Design**: No API-specific classes, works with any OpenAPI service
- **Minimal Dependencies**: Only requires PHP 8.0+ and cURL
-- **OAuth Support**: Built-in OAuth client for token management
+- **OAuth Support**: Built-in OAuth client for token management
- **HTTP Primitives**: GET, POST, PUT, DELETE, PATCH methods
- **Clean Interface**: Similar to the Rust SDK design
@@ -81,7 +82,7 @@ $client = new Client($token);
$params = ['denominazione' => 'Stellantis', 'provincia' => 'TO'];
$response = $client->get('https://test.company.openapi.com/IT-advanced', $params);
-// POST request
+// POST request
$payload = ['limit' => 10, 'query' => ['country_code' => 'IT']];
$response = $client->post('https://test.postontarget.com/fields/country', $payload);
@@ -91,6 +92,70 @@ $response = $client->delete($url);
$response = $client->patch($url, $payload);
```
+## Custom HTTP Clients (Guzzle, Laravel, etc.)
+
+By default, the SDK uses an internal cURL-based transport.
+However, you can now inject your own HTTP client, allowing full control over the request pipeline.
+
+This is especially useful in frameworks like Laravel, where you may want to reuse an existing HTTP client with middleware such as retry, caching, logging, or tracing.
+
+Using a custom HTTP client (e.g. Guzzle)
+
+You can pass any PSR-18 compatible client (such as Guzzle) directly to the SDK:
+
+```php
+use OpenApi\Client;
+use GuzzleHttp\Client as GuzzleClient;
+
+$guzzle = new GuzzleClient([
+ 'timeout' => 10,
+ // You can configure middleware, retry logic, caching, etc. here
+]);
+
+$client = new Client($token, $guzzle);
+
+$response = $client->get('https://test.company.openapi.com/IT-advanced', [
+ 'denominazione' => 'Stellantis',
+]);
+```
+
+### Why this matters
+
+When using the default transport, requests are executed via cURL and bypass any framework-level HTTP configuration.
+
+By injecting your own client, you can:
+
+- ✅ Reuse your existing HTTP middleware stack (e.g. Laravel retry/cache)
+- ✅ Centralize logging, tracing, and observability
+- ✅ Apply custom headers, timeouts, or authentication strategies
+- ✅ Maintain consistency with your application's HTTP layer
+
+### Custom Transport Interface
+
+If needed, you can also implement your own transport by using the provided interface:
+
+```php
+use OpenApi\Interfaces\HttpTransportInterface;
+
+class MyTransport implements HttpTransportInterface
+{
+ public function request(
+ string $method,
+ string $url,
+ mixed $payload = null,
+ ?array $params = null
+ ): string {
+ // Your custom implementation
+ }
+}
+```
+
+And inject it:
+
+```php
+$client = new Client($token, new MyTransport());
+```
+
## Architecture
This SDK follows a minimal approach with only essential components:
@@ -134,7 +199,6 @@ composer run test
composer run test:unit
```
-
## Contributing
Contributions are always welcome! Whether you want to report bugs, suggest new features, improve documentation, or contribute code, your help is appreciated.
@@ -165,9 +229,9 @@ Meet our partners using Openapi or contributing to this SDK:
## Our Commitments
-We believe in open source and we act on that belief. We became Silver Members
-of the Linux Foundation because we wanted to formally support the ecosystem
-we build on every day. Open standards, open collaboration, and open governance
+We believe in open source and we act on that belief. We became Silver Members
+of the Linux Foundation because we wanted to formally support the ecosystem
+we build on every day. Open standards, open collaboration, and open governance
are part of how we work and how we think about software.
## License
@@ -179,4 +243,3 @@ The MIT License is a permissive open-source license that allows you to freely us
In short, you are free to use this SDK in your personal, academic, or commercial projects, with minimal restrictions. The project is provided "as-is", without any warranty of any kind, either expressed or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement.
For more details, see the full license text at the [MIT License page](https://choosealicense.com/licenses/mit/).
-
diff --git a/composer.json b/composer.json
index 5e513da..7d9b539 100644
--- a/composer.json
+++ b/composer.json
@@ -1,5 +1,5 @@
{
- "name": "openapi/openapi-sdk",
+ "name": "seraphim/openapi-sdk",
"description": "Minimal and agnostic PHP SDK for Openapi® (https://openapi.com)",
"license": "MIT",
"authors": [
@@ -12,7 +12,8 @@
"require": {
"php": ">=8.0.0",
"ext-curl": "*",
- "ext-json": "*"
+ "ext-json": "*",
+ "psr/http-client": "^1.0"
},
"require-dev": {
"symfony/dotenv": "^5.3",
diff --git a/src/Client.php b/src/Client.php
index 6f7a80f..370202b 100644
--- a/src/Client.php
+++ b/src/Client.php
@@ -2,6 +2,11 @@
namespace OpenApi;
+use OpenApi\Interfaces\HttpTransportInterface;
+use OpenApi\Transports\CurlTransport;
+use Psr\Http\Client\ClientInterface as PsrClientInterface;;
+
+
/**
* Generic HTTP client for OpenAPI services
* Handles REST operations with Bearer token authentication
@@ -10,70 +15,25 @@ class Client
{
private string $token;
+ private HttpTransportInterface|PsrClientInterface $transport;
+
/**
* Initialize client with Bearer token
*/
- public function __construct(string $token)
+ public function __construct(string $token, HttpTransportInterface|PsrClientInterface|null $transport = null)
{
$this->token = $token;
+ $this->transport = $transport ?? new CurlTransport($token);
}
- /**
- * Execute HTTP request
- *
- * @param string $method HTTP method (GET, POST, PUT, DELETE, PATCH)
- * @param string $url Target URL
- * @param mixed $payload Request body (for POST/PUT/PATCH)
- * @param array|null $params Query parameters (for GET) or form data (for other methods)
- * @return string Response body
- */
- public function request(string $method, string $url, mixed $payload = null, ?array $params = null): string
- {
- // Append query parameters for GET requests
- if ($params && $method === 'GET') {
- $url .= '?' . http_build_query($params);
- }
-
- $ch = curl_init();
-
- curl_setopt_array($ch, [
- CURLOPT_URL => $url,
- CURLOPT_RETURNTRANSFER => true,
- CURLOPT_CUSTOMREQUEST => $method,
- CURLOPT_TIMEOUT => 30,
- CURLOPT_HTTPHEADER => [
- 'Content-Type: application/json',
- 'Authorization: Bearer ' . $this->token
- ]
- ]);
-
- // Add JSON payload for POST/PUT/PATCH requests
- if ($payload && in_array($method, ['POST', 'PUT', 'PATCH'])) {
- curl_setopt($ch, CURLOPT_POSTFIELDS, is_string($payload) ? $payload : json_encode($payload));
- }
-
- // Add form data for non-GET requests
- if ($params && $method !== 'GET') {
- curl_setopt($ch, CURLOPT_POSTFIELDS,
- is_string($params) ? $params : http_build_query($params));
- }
-
- $response = curl_exec($ch);
- $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
- $error = curl_error($ch);
- curl_close($ch);
-
- // TODO: Provide more graceful error message with connection context (timeout, DNS, SSL, etc.)
- if ($response === false) {
- throw new Exception("cURL Error: " . $error);
- }
-
- // TODO: Parse response body and provide structured error details (error code, message, request ID)
- if ($httpCode >= 400) {
- throw new Exception("HTTP Error {$httpCode}: " . $response);
- }
- return $response;
+ public function request(
+ string $method,
+ string $url,
+ mixed $payload = null,
+ ?array $params = null
+ ): string {
+ return $this->transport->request($method, $url, $payload, $params);
}
/**
diff --git a/src/Interfaces/HttpTransportInterface.php b/src/Interfaces/HttpTransportInterface.php
new file mode 100644
index 0000000..03dcdc3
--- /dev/null
+++ b/src/Interfaces/HttpTransportInterface.php
@@ -0,0 +1,13 @@
+ $url,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_CUSTOMREQUEST => $method,
+ CURLOPT_TIMEOUT => 30,
+ CURLOPT_HTTPHEADER => [
+ 'Content-Type: application/json',
+ 'Authorization: Bearer ' . $this->token,
+ ],
+ ]);
+
+ if ($payload && in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
+ curl_setopt($ch, CURLOPT_POSTFIELDS, is_string($payload) ? $payload : json_encode($payload));
+ }
+
+ if ($params && $method !== 'GET' && !$payload) {
+ curl_setopt($ch, CURLOPT_POSTFIELDS, is_string($params) ? $params : http_build_query($params));
+ }
+
+ $response = curl_exec($ch);
+ $error = curl_error($ch);
+ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ $ch = null;
+
+ if ($response === false) {
+ throw new \RuntimeException('cURL error: ' . $error);
+ }
+
+ if ($httpCode >= 400) {
+ throw new \RuntimeException("HTTP error {$httpCode}: {$response}");
+ }
+
+ return $response;
+ }
+}
\ No newline at end of file
diff --git a/tests/ClientTest.php b/tests/ClientTest.php
new file mode 100644
index 0000000..6819695
--- /dev/null
+++ b/tests/ClientTest.php
@@ -0,0 +1,47 @@
+request(
+ 'POST',
+ 'https://example.com/api/users',
+ ['name' => 'John'],
+ ['page' => 1]
+ );
+
+ $this->assertSame('fake-response', $response);
+
+ $this->assertSame('POST', $transport->lastMethod);
+ $this->assertSame('https://example.com/api/users', $transport->lastUrl);
+ $this->assertSame(['name' => 'John'], $transport->lastPayload);
+ $this->assertSame(['page' => 1], $transport->lastParams);
+ $this->assertSame(1, $transport->callCount);
+ }
+
+ public function test_it_calls_transport_once_per_request(): void
+ {
+ $transport = new FakeTransport();
+ $client = new Client('test-token', $transport);
+
+ $client->request('GET', 'https://example.com/one');
+ $client->request('GET', 'https://example.com/two');
+
+ $this->assertSame(2, $transport->callCount);
+ $this->assertSame('https://example.com/two', $transport->lastUrl);
+ }
+}
+
diff --git a/tests/Transports/FakeTransport.php b/tests/Transports/FakeTransport.php
new file mode 100644
index 0000000..ce67e21
--- /dev/null
+++ b/tests/Transports/FakeTransport.php
@@ -0,0 +1,29 @@
+callCount++;
+ $this->lastMethod = $method;
+ $this->lastUrl = $url;
+ $this->lastPayload = $payload;
+ $this->lastParams = $params;
+
+ return 'fake-response';
+ }
+}
\ No newline at end of file