Skip to content
Open
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
89 changes: 76 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
<h1>Openapi® client for PHP</h1>
<h4>The perfect starting point to integrate <a href="https://openapi.com/">Openapi®</a> within your PHP project</h4>

[![Build Status](https://github.com/openapi/openapi-php-sdk/actions/workflows/php.yml/badge.svg)](https://github.com/openapi/openapi-php-sdk/actions/workflows/php.yml)
[![Packagist Version](https://img.shields.io/packagist/v/openapi/openapi-sdk)](https://packagist.org/packages/openapi/openapi-sdk)
[![PHP Version](https://img.shields.io/packagist/php-v/openapi/openapi-sdk)](https://packagist.org/packages/openapi/openapi-sdk)
[![License](https://img.shields.io/github/license/openapi/openapi-php-sdk?v=2)](LICENSE)
[![Downloads](https://img.shields.io/packagist/dt/openapi/openapi-sdk)](https://packagist.org/packages/openapi/openapi-sdk)
<br>
[![Build Status](https://github.com/openapi/openapi-php-sdk/actions/workflows/php.yml/badge.svg)](https://github.com/openapi/openapi-php-sdk/actions/workflows/php.yml)
[![Packagist Version](https://img.shields.io/packagist/v/openapi/openapi-sdk)](https://packagist.org/packages/openapi/openapi-sdk)
[![PHP Version](https://img.shields.io/packagist/php-v/openapi/openapi-sdk)](https://packagist.org/packages/openapi/openapi-sdk)
[![License](https://img.shields.io/github/license/openapi/openapi-php-sdk?v=2)](LICENSE)
[![Downloads](https://img.shields.io/packagist/dt/openapi/openapi-sdk)](https://packagist.org/packages/openapi/openapi-sdk)
<br>
[![Linux Foundation Member](https://img.shields.io/badge/Linux%20Foundation-Silver%20Member-003778?logo=linux-foundation&logoColor=white)](https://www.linuxfoundation.org/about/members)

</div>

## Overview
Expand All @@ -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

Expand Down Expand Up @@ -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);

Expand All @@ -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:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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/).

5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
@@ -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": [
Expand All @@ -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",
Expand Down
72 changes: 16 additions & 56 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}

/**
Expand Down
13 changes: 13 additions & 0 deletions src/Interfaces/HttpTransportInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace OpenApi\Interfaces;

interface HttpTransportInterface
{
public function request(
string $method,
string $url,
mixed $payload = null,
?array $params = null
): string;
}
68 changes: 68 additions & 0 deletions src/Transports/CurlTransport.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

namespace OpenApi\Transports;

use OpenApi\Interfaces\HttpTransportInterface;

final class CurlTransport implements HttpTransportInterface
{
public function __construct(
private ?string $token = null
) {}

/**
* 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 {
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,
],
]);

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;
}
}
47 changes: 47 additions & 0 deletions tests/ClientTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace Tests;

use OpenApi\Client;
use Tests\Transports\FakeTransport;
use PHPUnit\Framework\TestCase;

final class ClientTest extends TestCase
{
public function test_it_uses_injected_transport_for_requests(): void
{
$transport = new FakeTransport();

$client = new Client('test-token', $transport);

$response = $client->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);
}
}

Loading