From 44e28e5fac65b60501be143aa12cf2fd122aef23 Mon Sep 17 00:00:00 2001 From: Mario Ugurcu Date: Sat, 28 Mar 2026 13:16:34 +0100 Subject: [PATCH 1/4] feat: support custom HTTP client injection Allow passing a pre-configured HTTP client (e.g. Guzzle) to the Client. This enables reuse of middleware stacks (e.g. Laravel retry/cache) instead of forcing the internal cURL transport. --- README.md | 89 +++++++++++++++++++---- composer.json | 5 +- src/Client.php | 72 ++++-------------- src/Interfaces/HttpTransportInterface.php | 13 ++++ src/Transports/CurlTransport.php | 68 +++++++++++++++++ tests/ClientTest.php | 47 ++++++++++++ tests/Transports/FakeTransport.php | 29 ++++++++ 7 files changed, 252 insertions(+), 71 deletions(-) create mode 100644 src/Interfaces/HttpTransportInterface.php create mode 100644 src/Transports/CurlTransport.php create mode 100644 tests/ClientTest.php create mode 100644 tests/Transports/FakeTransport.php 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

- [![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) -
+[![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) +
[![Linux Foundation Member](https://img.shields.io/badge/Linux%20Foundation-Silver%20Member-003778?logo=linux-foundation&logoColor=white)](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 From c0abd617c7d7fc43ba7885074f420b70224bdc61 Mon Sep 17 00:00:00 2001 From: Mario Ugurcu Date: Thu, 9 Apr 2026 09:16:31 +0200 Subject: [PATCH 2/4] Update composer.json Added Tests namespace --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 7d9b539..53450a2 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,8 @@ }, "autoload": { "psr-4": { - "OpenApi\\": "src" + "OpenApi\\": "src", + "Tests\\": "tests" } }, "scripts": { From 6d7acf1292d40f9a54ad0cdcdbe4bbe10996fa37 Mon Sep 17 00:00:00 2001 From: Mario Ugurcu Date: Thu, 9 Apr 2026 11:07:57 +0200 Subject: [PATCH 3/4] Update composer.json load Tests Namespace only in autoload-dev --- composer.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 53450a2..ec843fa 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,11 @@ }, "autoload": { "psr-4": { - "OpenApi\\": "src", + "OpenApi\\": "src" + } + }, + "autoload-dev": { + "psr-4": { "Tests\\": "tests" } }, From 4a3bbb107ceffb2ecea3db8e01655300918bc1a9 Mon Sep 17 00:00:00 2001 From: Mario Ugurcu Date: Tue, 14 Apr 2026 13:10:14 +0200 Subject: [PATCH 4/4] Updated naming according to the convention and working on #12. --- README.md | 54 +++++++ composer.json | 10 +- examples/api_calls.php | 4 +- examples/complete_workflow.php | 14 +- examples/token_generation.php | 4 +- phpunit.xml.dist | 20 +++ .../{ArrayCache.php => OpenapiArrayCache.php} | 4 +- ...nterface.php => OpenapiCacheInterface.php} | 4 +- src/Environment/OpenapiDotEnv.php | 133 ++++++++++++++++++ src/Interfaces/OpenapiDotEnvInterface.php | 7 + ....php => OpenapiHttpTransportInterface.php} | 4 +- src/OpenapiBootstrap.php | 59 ++++++++ src/{Client.php => OpenapiClient.php} | 31 ++-- src/{Exception.php => OpenapiException.php} | 6 +- ...OauthClient.php => OpenapiOauthClient.php} | 19 +-- ...Transport.php => OpenapiCurlTransport.php} | 6 +- tests/OauthClientTest.php | 38 ++++- ...lientTest.php => OpenapiApiClientTest.php} | 16 +-- ...acheTest.php => OpenapiArrayCacheTest.php} | 6 +- .../{ClientTest.php => OpenapiClientTest.php} | 24 +++- ...ptionTest.php => OpenapiExceptionTest.php} | 8 +- ...Transport.php => OpenapiFakeTransport.php} | 4 +- 22 files changed, 402 insertions(+), 73 deletions(-) create mode 100644 phpunit.xml.dist rename src/Cache/{ArrayCache.php => OpenapiArrayCache.php} (93%) rename src/Cache/{CacheInterface.php => OpenapiCacheInterface.php} (91%) create mode 100644 src/Environment/OpenapiDotEnv.php create mode 100644 src/Interfaces/OpenapiDotEnvInterface.php rename src/Interfaces/{HttpTransportInterface.php => OpenapiHttpTransportInterface.php} (66%) create mode 100644 src/OpenapiBootstrap.php rename src/{Client.php => OpenapiClient.php} (59%) rename src/{Exception.php => OpenapiException.php} (93%) rename src/{OauthClient.php => OpenapiOauthClient.php} (85%) rename src/Transports/{CurlTransport.php => OpenapiCurlTransport.php} (89%) rename tests/{ApiClientTest.php => OpenapiApiClientTest.php} (81%) rename tests/{ArrayCacheTest.php => OpenapiArrayCacheTest.php} (92%) rename tests/{ClientTest.php => OpenapiClientTest.php} (59%) rename tests/{ExceptionTest.php => OpenapiExceptionTest.php} (82%) rename tests/Transports/{FakeTransport.php => OpenapiFakeTransport.php} (78%) diff --git a/README.md b/README.md index b2f7a9f..2e15efb 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Before using the Openapi PHP Client, you will need an account at [Openapi](https - **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 +- **Built-in DotEnv Support**: Lightweight environment loader with safe fallback behavior ## What you can do @@ -165,6 +166,59 @@ This SDK follows a minimal approach with only essential components: - `Exception`: Error handling - `Cache\CacheInterface`: Optional caching interface +## Environment Configuration (.env support) + +This SDK includes a lightweight and framework-agnostic .env loader to simplify configuration in non-framework environments. + +### Automatic loading + +When installed via Composer, the SDK will automatically attempt to load a .env file from the project root. + +This happens only when no existing environment configuration is detected (e.g. Laravel, Symfony, CI environments). + +- ✅ Does not override existing environment variables +- ✅ Works out of the box in plain PHP projects +- ✅ Compatible with Laravel, Symfony, and other frameworks +- ✅ Safe fallback mechanism + +### Supported variables + +The following environment variables are commonly used: + +```env +OPENAPI_BASE_URL=https://example.com +OPENAPI_OAUTH_USERNAME=your_username +OPENAPI_OAUTH_APIKEY=your_api_key +OPENAPI_OAUTH_URL=https://api.com +OPENAPI_OAUTH_TEST_URL=https://api.com +``` + +### Framework compatibility + +If you are using a framework like Laravel or Symfony: + +- The SDK will not override your existing environment +- Your framework's configuration system remains the source of truth +- The internal loader acts only as a fallback + +### Manual usage + +If you prefer full control, you can use the DotEnv loader manually: + +```php +use OpenApi\Environment\DotEnv\DotEnv; + +$dotenv = new DotEnv(__DIR__ . '/.env'); +$dotenv->load(); + +``` + +### Notes + +- The loader is intentionally minimal and does not aim to fully replace libraries like vlucas/phpdotenv +- Designed for performance, predictability, and zero external dependencies +- Suitable for CLI tools, microservices, and lightweight integrations + ## Requirements - PHP 8.0 or higher diff --git a/composer.json b/composer.json index 7d9b539..4e47db3 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,15 @@ }, "autoload": { "psr-4": { - "OpenApi\\": "src" + "Openapi\\": "src" + }, + "files": [ + "src/OpenapiBootstrap.php" + ] + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests" } }, "scripts": { diff --git a/examples/api_calls.php b/examples/api_calls.php index 4be6279..5ff9660 100644 --- a/examples/api_calls.php +++ b/examples/api_calls.php @@ -2,11 +2,11 @@ require_once __DIR__ . '/../vendor/autoload.php'; -use OpenApi\Client; +use OpenApi\OpenapiClient; try { $token = ''; - $client = new Client($token); + $client = new OpenapiClient($token); // GET request with parameters $params = [ diff --git a/examples/complete_workflow.php b/examples/complete_workflow.php index a6389cf..43eca3d 100644 --- a/examples/complete_workflow.php +++ b/examples/complete_workflow.php @@ -2,16 +2,16 @@ require_once __DIR__ . '/../vendor/autoload.php'; -use OpenApi\OauthClient; -use OpenApi\Client; -use OpenApi\Exception; +use OpenApi\OpenapiOauthClient; +use OpenApi\OpenapiClient; +use OpenApi\OpenapiException; try { echo "=== OpenAPI PHP SDK Complete Workflow Example ===" . PHP_EOL . PHP_EOL; // Step 1: Create OAuth client echo "Step 1: Creating OAuth client..." . PHP_EOL; - $oauthClient = new OauthClient('', '', true); + $oauthClient = new OpenapiOauthClient('', '', true); echo "✓ OAuth client created" . PHP_EOL . PHP_EOL; // Step 2: Generate token @@ -25,7 +25,7 @@ $tokenData = json_decode($tokenResult, true); if (!isset($tokenData['token'])) { - throw new Exception('Failed to generate token: ' . $tokenResult); + throw new OpenapiException('Failed to generate token: ' . $tokenResult); } $token = $tokenData['token']; @@ -33,7 +33,7 @@ // Step 3: Create API client echo "Step 3: Creating API client..." . PHP_EOL; - $apiClient = new Client($token); + $apiClient = new OpenapiClient($token); echo "✓ API client created" . PHP_EOL . PHP_EOL; // Step 4: Make API calls @@ -62,7 +62,7 @@ echo "=== Workflow completed successfully! ===" . PHP_EOL; -} catch (Exception $e) { +} catch (OpenapiException $e) { echo "✗ Error: " . $e->getMessage() . PHP_EOL; if ($e->getHttpCode()) { diff --git a/examples/token_generation.php b/examples/token_generation.php index 79c08b2..24b30ea 100644 --- a/examples/token_generation.php +++ b/examples/token_generation.php @@ -2,10 +2,10 @@ require_once __DIR__ . '/../vendor/autoload.php'; -use OpenApi\OauthClient; +use OpenApi\OpenapiOauthClient; try { - $oauthClient = new OauthClient('', '', true); + $oauthClient = new OpenapiOauthClient('', '', true); $scopes = [ 'GET:test.imprese.openapi.it/advance', diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..d8d04ba --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,20 @@ + + + + + + tests + + + + + + + + + + + \ No newline at end of file diff --git a/src/Cache/ArrayCache.php b/src/Cache/OpenapiArrayCache.php similarity index 93% rename from src/Cache/ArrayCache.php rename to src/Cache/OpenapiArrayCache.php index c52170e..87f5f0b 100644 --- a/src/Cache/ArrayCache.php +++ b/src/Cache/OpenapiArrayCache.php @@ -1,12 +1,12 @@ path = $path; + } + + /** + * Loads environment variables from the configured .env file. + * + * Empty lines and comment lines are ignored. Only lines containing a key-value + * separator are processed. Variables that already exist in {@see $_ENV}, + * {@see $_SERVER}, or the process environment are left untouched. + * + * @return void + * + * @throws \RuntimeException Thrown when the file is not readable. + * @throws \RuntimeException Thrown when the file contents cannot be read. + */ + public function load() :void + { + if (!is_readable($this->path)) { + throw new \RuntimeException(sprintf('%s file is not readable', $this->path)); + } + + $lines = file($this->path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + + if ($lines === false) { + throw new \RuntimeException(sprintf('Unable to read %s', $this->path)); + } + + foreach ($lines as $line) { + $line = trim($line); + + if ($line === '' || strpos($line, '#') === 0) { + continue; + } + + if (strpos($line, '=') === false) { + continue; + } + + [$name, $value] = explode('=', $line, 2); + + $name = trim($name); + $value = $this->normalizeValue(trim($value)); + + if ($name === '') { + continue; + } + + if ($this->isLoaded($name)) { + continue; + } + + putenv(sprintf('%s=%s', $name, $value)); + $_ENV[$name] = $value; + $_SERVER[$name] = $value; + } + } + + /** + * Determines whether the given environment variable is already available. + * + * A variable is considered loaded when it exists in {@see $_ENV}, + * {@see $_SERVER}, or the process environment. + * + * @param string $name Environment variable name. + * + * @return bool True when the variable is already present; otherwise false. + */ + private function isLoaded(string $name): bool + { + return array_key_exists($name, $_ENV) + || array_key_exists($name, $_SERVER) + || getenv($name) !== false; + } + + /** + * Normalizes a parsed environment variable value. + * + * Matching single or double quotes wrapping the full value are removed. + * All other values are returned unchanged. + * + * @param string $value Raw environment variable value. + * + * @return string Normalized environment variable value. + */ + private function normalizeValue(string $value): string + { + $length = strlen($value); + + if ($length >= 2) { + $first = $value[0]; + $last = $value[$length - 1]; + + if (($first === '"' && $last === '"') || ($first === "'" && $last === "'")) { + return substr($value, 1, -1); + } + } + + return $value; + } +} diff --git a/src/Interfaces/OpenapiDotEnvInterface.php b/src/Interfaces/OpenapiDotEnvInterface.php new file mode 100644 index 0000000..e685ca5 --- /dev/null +++ b/src/Interfaces/OpenapiDotEnvInterface.php @@ -0,0 +1,7 @@ +getFileName(), 2); + $projectRoot = dirname($vendorDir); + + return is_file($projectRoot . DIRECTORY_SEPARATOR . '.env') ? $projectRoot : null; + } +} + +$envFile = findProjectRoot(__DIR__) . DIRECTORY_SEPARATOR . ".env"; +if (is_file($envFile)) { + (new OpenapiDotEnv($envFile))->load(); +} \ No newline at end of file diff --git a/src/Client.php b/src/OpenapiClient.php similarity index 59% rename from src/Client.php rename to src/OpenapiClient.php index 370202b..8e78b30 100644 --- a/src/Client.php +++ b/src/OpenapiClient.php @@ -1,29 +1,34 @@ token = $token; - $this->transport = $transport ?? new CurlTransport($token); + $this->token = $token ?? getenv('OPEN_API_TOKEN') ; + if(getenv("OPENAPI_BASE_URL")){ + $this->baseUrl = getenv("OPENAPI_BASE_URL"); + } + $this->transport = $transport ?? new OpenapiCurlTransport($token); } @@ -33,6 +38,12 @@ public function request( mixed $payload = null, ?array $params = null ): string { + $isAbsolute = str_starts_with(strtolower($url), 'http'); + + // Wenn nicht absolut und baseUrl vorhanden -> Zusammenfügen + if (!$isAbsolute && !empty($this->baseUrl)) { + $url = rtrim($this->baseUrl, '/') . '/' . ltrim($url, '/'); + } return $this->transport->request($method, $url, $payload, $params); } @@ -75,4 +86,4 @@ public function patch(string $url, mixed $payload = null): string { return $this->request('PATCH', $url, $payload); } -} \ No newline at end of file +} diff --git a/src/Exception.php b/src/OpenapiException.php similarity index 93% rename from src/Exception.php rename to src/OpenapiException.php index 74650d1..7159666 100644 --- a/src/Exception.php +++ b/src/OpenapiException.php @@ -1,12 +1,12 @@ username = $username; - $this->apikey = $apikey; - $this->url = $test ? self::TEST_OAUTH_BASE_URL : self::OAUTH_BASE_URL; + $this->username = $username ?? getenv('OPENAPI_OAUTH_USERNAME') ; + $this->apikey = $apikey ?? getenv('OPENAPI_OAUTH_APIKEY'); + $this->url = $test ? getenv('OPENAPI_OAUTH_TEST_URL') ?? self::TEST_OAUTH_BASE_URL + : getenv('OPENAPI_OAUTH_URL') ?? self::OAUTH_BASE_URL; } /** @@ -126,12 +127,12 @@ private function request(string $method, string $url, array $body = null): strin // TODO: Provide more graceful error message with connection context (timeout, DNS, SSL, etc.) if ($response === false) { - throw new Exception("cURL Error: " . $error); + throw new OpenapiException("cURL Error: " . $error); } // TODO: Parse response body and provide structured error details with auth-specific hints (invalid credentials, expired key, etc.) if ($httpCode >= 400) { - throw new Exception("HTTP Error {$httpCode}: " . $response); + throw new OpenapiException("HTTP Error {$httpCode}: " . $response); } return $response; diff --git a/src/Transports/CurlTransport.php b/src/Transports/OpenapiCurlTransport.php similarity index 89% rename from src/Transports/CurlTransport.php rename to src/Transports/OpenapiCurlTransport.php index 18a60a4..2bbec7d 100644 --- a/src/Transports/CurlTransport.php +++ b/src/Transports/OpenapiCurlTransport.php @@ -1,10 +1,10 @@ username, $this->apikey, true); - $this->assertInstanceOf(OauthClient::class, $client); + $client = new OpenapiOauthClient($this->username, $this->apikey, true); + $this->assertInstanceOf(OpenapiOauthClient::class, $client); + } + + public function testOauthClientCanBeCreatedFromEnvironmentVariables(): void + { + $username = getenv('OPENAPI_USERNAME'); + $apikey = getenv('OPENAPI_SANDBOX_KEY'); + + $this->assertNotFalse($username, 'OPENAPI_OAUTH_USERNAME is not set'); + $this->assertNotFalse($apikey, 'OPENAPI_OAUTH_APIKEY is not set'); + $this->assertNotSame('', $username, 'OPENAPI_OAUTH_USERNAME is empty'); + $this->assertNotSame('', $apikey, 'OPENAPI_OAUTH_APIKEY is empty'); + + $client = new OpenapiOauthClient($username, $apikey, true); + + $this->assertInstanceOf(OpenapiOauthClient::class, $client); } public function testOauthClientProductionMode(): void { - $client = new OauthClient($this->username, $this->apikey, false); - $this->assertInstanceOf(OauthClient::class, $client); + $client = new OpenapiOauthClient($this->username, $this->apikey, false); + $this->assertInstanceOf(OpenapiOauthClient::class, $client); + } + + public function testEnvironmentVariablesAreAvailable(): void + { + $this->assertSame('test_user', getenv('OPENAPI_USERNAME')); + $this->assertSame('test_key', getenv('OPENAPI_SANDBOX_KEY')); + $this->assertSame('https://api.com', getenv('OPENAPI_OAUTH_SANDBOX_URL')); + $this->assertSame('https://api.com', getenv('OPENAPI_OAUTH_URL')); + $this->assertSame('https://example.com', getenv('OPENAPI_BASE_URL')); } public function testCreateTokenWithScopes(): void { $this->markTestSkipped('Requires valid credentials for integration test'); - $client = new OauthClient($this->username, $this->apikey, true); + $client = new OpenapiOauthClient($this->username, $this->apikey, true); $scopes = [ 'GET:test.imprese.openapi.it/advance', 'POST:test.postontarget.com/fields/country' diff --git a/tests/ApiClientTest.php b/tests/OpenapiApiClientTest.php similarity index 81% rename from tests/ApiClientTest.php rename to tests/OpenapiApiClientTest.php index 89e926c..792fcda 100644 --- a/tests/ApiClientTest.php +++ b/tests/OpenapiApiClientTest.php @@ -1,6 +1,6 @@ testToken); - $this->assertInstanceOf(Client::class, $client); + $client = new OpenapiClient($this->testToken); + $this->assertInstanceOf(OpenapiClient::class, $client); } public function testGetRequest(): void { $this->markTestSkipped('Requires valid token for integration test'); - $client = new Client($this->testToken); + $client = new OpenapiClient($this->testToken); $params = [ 'denominazione' => 'altravia', 'provincia' => 'RM', @@ -32,7 +32,7 @@ public function testPostRequest(): void { $this->markTestSkipped('Requires valid token for integration test'); - $client = new Client($this->testToken); + $client = new OpenapiClient($this->testToken); $payload = [ 'limit' => 10, 'query' => [ @@ -48,7 +48,7 @@ public function testPutRequest(): void { $this->markTestSkipped('Requires valid token for integration test'); - $client = new Client($this->testToken); + $client = new OpenapiClient($this->testToken); $payload = ['test' => 'data']; $result = $client->put('https://example.com/api', $payload); @@ -59,7 +59,7 @@ public function testDeleteRequest(): void { $this->markTestSkipped('Requires valid token for integration test'); - $client = new Client($this->testToken); + $client = new OpenapiClient($this->testToken); $result = $client->delete('https://example.com/api/123'); $this->assertIsString($result); @@ -69,7 +69,7 @@ public function testPatchRequest(): void { $this->markTestSkipped('Requires valid token for integration test'); - $client = new Client($this->testToken); + $client = new OpenapiClient($this->testToken); $payload = ['update' => 'data']; $result = $client->patch('https://example.com/api/123', $payload); diff --git a/tests/ArrayCacheTest.php b/tests/OpenapiArrayCacheTest.php similarity index 92% rename from tests/ArrayCacheTest.php rename to tests/OpenapiArrayCacheTest.php index 9e22756..fe6fd97 100644 --- a/tests/ArrayCacheTest.php +++ b/tests/OpenapiArrayCacheTest.php @@ -1,15 +1,15 @@ cache = new ArrayCache(); + $this->cache = new OpenapiArrayCache(); } public function testCacheImplementation(): void diff --git a/tests/ClientTest.php b/tests/OpenapiClientTest.php similarity index 59% rename from tests/ClientTest.php rename to tests/OpenapiClientTest.php index 6819695..90b5788 100644 --- a/tests/ClientTest.php +++ b/tests/OpenapiClientTest.php @@ -4,17 +4,17 @@ namespace Tests; -use OpenApi\Client; -use Tests\Transports\FakeTransport; +use Openapi\OpenapiClient; +use Tests\Transports\OpenapiFakeTransport; use PHPUnit\Framework\TestCase; final class ClientTest extends TestCase { public function test_it_uses_injected_transport_for_requests(): void { - $transport = new FakeTransport(); + $transport = new OpenapiFakeTransport(); - $client = new Client('test-token', $transport); + $client = new OpenapiClient('test-token', $transport); $response = $client->request( 'POST', @@ -34,8 +34,8 @@ public function test_it_uses_injected_transport_for_requests(): void public function test_it_calls_transport_once_per_request(): void { - $transport = new FakeTransport(); - $client = new Client('test-token', $transport); + $transport = new OpenapiFakeTransport(); + $client = new OpenapiClient('test-token', $transport); $client->request('GET', 'https://example.com/one'); $client->request('GET', 'https://example.com/two'); @@ -43,5 +43,17 @@ public function test_it_calls_transport_once_per_request(): void $this->assertSame(2, $transport->callCount); $this->assertSame('https://example.com/two', $transport->lastUrl); } + + public function test_it_use_dot_env_for_request(): void + { + $transport = new OpenapiFakeTransport(); + $client = new OpenapiClient('test-token', $transport); + + $client->request('GET', '/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/ExceptionTest.php b/tests/OpenapiExceptionTest.php similarity index 82% rename from tests/ExceptionTest.php rename to tests/OpenapiExceptionTest.php index daa4438..55c070c 100644 --- a/tests/ExceptionTest.php +++ b/tests/OpenapiExceptionTest.php @@ -1,16 +1,16 @@ assertEquals($message, $exception->getMessage()); $this->assertEquals($code, $exception->getCode()); @@ -18,7 +18,7 @@ public function testExceptionCreation(): void public function testSetServerResponse(): void { - $exception = new Exception('Test message'); + $exception = new OpenapiException('Test message'); $response = ['error' => 'Server error']; $headers = 'Content-Type: application/json'; diff --git a/tests/Transports/FakeTransport.php b/tests/Transports/OpenapiFakeTransport.php similarity index 78% rename from tests/Transports/FakeTransport.php rename to tests/Transports/OpenapiFakeTransport.php index ce67e21..727ab53 100644 --- a/tests/Transports/FakeTransport.php +++ b/tests/Transports/OpenapiFakeTransport.php @@ -1,10 +1,10 @@