From 5a52c1d725d8de48d3f2bc96440e7f4cddb48d24 Mon Sep 17 00:00:00 2001 From: SP Date: Wed, 25 Jun 2025 15:19:47 +0200 Subject: [PATCH] Fix: Add getList, refactor and optimize --- README.md | 220 +++---- composer.json | 5 +- composer.lock | 16 +- phpunit.xml | 19 + src/RestAPIWrapperProffix/Client.php | 7 +- .../HttpClient/HttpClient.php | 590 ++++-------------- tests/.phpunit.result.cache | 1 - tests/Integration/ClientIntegrationTest.php | 24 +- .../RestAPIWrapperProffix/HttpClientTest.php | 44 -- tests/phpunit.xml | 23 - 10 files changed, 291 insertions(+), 658 deletions(-) create mode 100644 phpunit.xml delete mode 100644 tests/.phpunit.result.cache delete mode 100644 tests/RestAPIWrapperProffix/HttpClientTest.php delete mode 100644 tests/phpunit.xml diff --git a/README.md b/README.md index 49dcb6d..ea1b20e 100644 --- a/README.md +++ b/README.md @@ -1,204 +1,206 @@ - - # PHP Wrapper für PROFFIX REST-API -Ein effizienter PHP Wrapper für die PROFFIX REST-API +Ein effizienter PHP Wrapper für die PROFFIX REST-API. ![alt text](https://raw.githubusercontent.com/pitwch/php-wrapper-proffix-restapi/master/php-wrapper-proffix-rest.jpg "PHP Wrapper PROFFIX REST API") -### Installation -Der Wrapper kann entweder geklont oder via [Composer](https://getcomposer.org) installiert werden. +## Installation + +Der Wrapper kann via [Composer](https://getcomposer.org) installiert werden. ```php composer require pitwch/rest-api-wrapper-proffix-php ``` +## Konfiguration -#### Variante 1: Verwendung mit Composer (empfohlen) - +### Initialisierung -Autoload RestAPIWrapperProffix class: +Autoload der `RestAPIWrapperProffix` Klasse: ```php require __DIR__ . '/vendor/autoload.php'; use Pitwch\RestAPIWrapperProffix\Client; - ``` - -#### Konfiguration - Die Konfiguration wird dem Client mitgegeben: | Konfiguration | Beispiel | Bemerkung | |------------------|----------------------------------------------------------------------------------|--------------------------------------------------| -| url | https://myserver.ch:999 | URL der REST-API **ohne pxapi/v2/** | -| apiDatabase | DEMO | Name der Datenbank | -| apiUser | USR | Names des Benutzers | -| apiPassword | b62cce2fe18f7a156a9c...0f0d7bd18d9e8a40be2e663017 | SHA256-Hash des Benutzerpasswortes | -| apiModule | ADR,STU | Benötigte Module (mit Komma getrennt) | -| options | array('key'=>'112a5a90...59028') | Optionen (Details unter Optionen) | +| url | `https://myserver.ch:999` | URL der REST-API **ohne pxapi/v2/** | +| apiDatabase | `DEMO` | Name der Datenbank | +| apiUser | `USR` | Name des Benutzers | +| apiPassword | `b62cce2fe18f7a156a9c...` | SHA256-Hash des Benutzerpasswortes | +| apiModule | `ADR,STU` | Benötigte Module (mit Komma getrennt) | +| options | `array('key'=>'112a5a90...')` | Optionen (Details unter Optionen) | +### Beispiel für die Initialisierung -Beispiel: ```php - require __DIR__ . '/vendor/autoload.php'; use Pitwch\RestAPIWrapperProffix\Client; -$pxrest = new Client( +$pxrest = new Client( 'https://myserver.ch:999', 'DEMO', 'USR', 'b62cce2fe18f7a156a9c719c57bebf0478a3d50f0d7bd18d9e8a40be2e663017', 'ADR,STU', - array('key'=>'112a5a90fe28b23ed2c776562a7d1043957b5b79fad242b10141254b4de59028','limit'=>2)); -$adressen = $pxrest->get('ADR/Adresse',array('filter'=>'GeaendertAm>d\'2018-05-17 14:54:56\'','depth'=>1,'fields'=>'AdressNr,Name,GeaendertAm'));; + ['key'=>'112a5a90fe28b23ed2c776562a7d1043957b5b79fad242b10141254b4de59028','limit'=>2] +); + +$adressen = $pxrest->get('ADR/Adresse', ['filter'=>'GeaendertAm>d\'2018-05-17 14:54:56\'', 'depth'=>1, 'fields'=>'AdressNr,Name,GeaendertAm']); print_r($adressen); ``` -### Optionen + +## Optionen Optionen sind **fakultativ** und werden in der Regel nicht benötigt: -| Option | Beispiel | Bemerkung | -|------------------|------------------------------------------------------------------|----------------------------------------------------------------| -| key | 112a5a90fe28b...242b10141254b4de59028 | API-Key als SHA256 - Hash (kann auch direkt mitgegeben werden) | -| version | v2 | API-Version; Standard = v2 | -| api_prefix | /pxapi/ | Prefix für die API; Standard = /pxapi/ | -| login_endpoint | PRO/Login | Endpunkt für Login; Standard = PRO/Login | -| user_agent | php-wrapper-proffix-restapi | User Agent; Standard = php-wrapper-proffix-restapi | -| timeout | 15 | Timeout für Curl in Sekunden; Standard = 15 | -| follow_redirects | true | Weiterleitungen der API folgen; Standard = false | +| Option | Beispiel | Bemerkung | +|------------------|----------------------------------------|----------------------------------------------------------------| +| key | `112a5a90fe28b...` | API-Key als SHA256 - Hash (kann auch direkt mitgegeben werden) | +| version | `v2` | API-Version; Standard = v2 | +| api_prefix | `/pxapi/` | Prefix für die API; Standard = /pxapi/ | +| login_endpoint | `PRO/Login` | Endpunkt für Login; Standard = PRO/Login | +| user_agent | `php-wrapper-proffix-restapi` | User Agent; Standard = php-wrapper-proffix-restapi | +| timeout | `15` | Timeout für Curl in Sekunden; Standard = 15 | +| follow_redirects | `true` | Weiterleitungen der API folgen; Standard = false | -#### Methoden +## Methoden +### Allgemeine Methoden (`get`, `put`, `post`, `delete`) | Parameter | Typ | Bemerkung | |------------|--------|----------------------------------------------------------------------------------------------------------| -| endpoint | string | Endpunkt der PROFFIX REST-API; z.B. ADR/Adresse,STU/Rapporte... | -| data | array | Daten (werden automatisch in JSON konvertiert); z.B: array("Name"=>"Demo AG",...) | -| parameters | array | Parameter gemäss [PROFFIX REST API Docs](http://www.proffix.net/Portals/0/content/REST%20API/index.html) | - +| endpoint | `string` | Endpunkt der PROFFIX REST-API; z.B. `ADR/Adresse`, `STU/Rapporte`... | +| data | `array` | Daten (werden automatisch in JSON konvertiert); z.B: `["Name"=>"Demo AG",...]` | +| parameters | `array` | Parameter gemäss [PROFFIX REST API Docs](http://www.proffix.net/Portals/0/content/REST%20API/index.html) | *Sonderzeichen in den Parametern müssen gegebenfalls mit Escape-Zeichen verwendet werden, z.B:* ```php -//Escape ' with \' -array('filter'=>'GeaendertAm>d\'2018-05-17 14:54:56\'','depth'=>1,'fields'=>'AdressNr,Name,GeaendertAm') +// Escape ' with \' +$params = ['filter' => 'GeaendertAm>d\'2018-05-17 14:54:56\'', 'depth' => 1, 'fields' => 'AdressNr,Name,GeaendertAm']; +$pxrest->get('ADR/Adresse', $params); ``` +#### Get / Query -Folgende unterschiedlichen Methoden sind mit dem Wrapper möglich: - +```php +// Einfache Abfrage +$adresse = $pxrest->get("ADR/Adresse/1"); +echo $adresse->Name; // DEMO AG +// Abfrage mit Parametern +$params = ['filter'=>'GeaendertAm>d\'2018-05-17 14:54:56\'','depth'=>1,'fields'=>'AdressNr,Name,GeaendertAm','limit'=>5]; +$adressen = $pxrest->get("ADR/Adresse", $params); +``` -##### Get / Query +#### Put / Update ```php -//Einfache Abfrage -$pxrest = new Client(...) -$adresse = $pxrest->get("ADR/Adresse/1") //Legt Response als Objects in $adresse ab -$adresse->Name //DEMO AG - -/Abfrage mit Parametern -$pxrest = new Client(...) -$adresse = $pxrest->get("ADR/Adresse",array('filter'=>'GeaendertAm>d\'2018-05-17 14:54:56\'','depth'=>1,'fields'=>'AdressNr,Name,GeaendertAm','limit'=>5)) - +$data = ["AdressNr"=>1, "Ort"=>"Zürich", "PLZ"=>8000, "EMail"=>"test@test.com"]; +$adresse = $pxrest->put("ADR/Adresse", $data); ``` - -##### Put / Update +#### Post / Create ```php -$pxrest = new Client(...) -$data = array("AdressNr"=>1,"Ort"=>"Zürich","PLZ"=>8000,"EMail"=>"test@test.com"); -$adresse = $pxrest->put("ADR/Adresse",$data) //Sendet $data an Endpunkt ADR/Adresse +$data = ["Ort"=>"Zürich", "PLZ"=>8000, "EMail"=>"test@test.com"]; +$neueAdresse = $pxrest->post("ADR/Adresse", $data); ``` -##### Post / Create +#### Delete ```php -$pxrest = new Client(...) -$data = array("AdressNr"=>1,"Ort"=>"Zürich","PLZ"=>8000,"EMail"=>"test@test.com"); -$adresse = $pxrest->post("ADR/Adresse",$data) //Sendet $data an Endpunkt ADR/Adresse +$response = $pxrest->delete("ADR/Adresse/42"); ``` +### Spezifische Methoden -##### Response / Antwort +#### `getList(int $listenr, array $body = [])` -Alle Methoden geben die Response als Array bzw. NULL (z.B. bei DELETE) -Bei Fehlern wird `HttpClientException` mit Rückmeldung der PROFFIX REST-API ausgegeben. +Generiert eine PROFFIX-Liste (z.B. ein PDF) und gibt das Ergebnis als `Response`-Objekt zurück, welches den rohen Dateiinhalt enthält. -Zudem lassen sich Zusatzinformationen zur Response wie folgt ausgeben: +| Parameter | Typ | Bemerkung | +|-----------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| +| `$listenr`| `int` | Die `ListeNr` der Liste, die generiert werden soll. | +| `$body` | `array` | (Optional) Ein assoziatives Array mit Parametern für die Listengenerierung. **Wichtig:** Es muss mindestens ein leeres JSON-Objekt (`{}`) gesendet werden. | -```php -$pxrest = new Client(...) -$adresse = $pxrest->get("ADR/Adresse") - -//Zusatzinformationen zum letzten Request -$lastRequest = $pxrest->http->getRequest(); -$lastRequest->getUrl(); // Get requested URL (string). -$lastRequest->getMethod(); // Get request method (string). -$lastRequest->getParameters(); // Get request parameters (array). -$lastRequest->getHeaders(); // Get request headers (array). -$lastRequest->getBody(); // Get request body (JSON). +*Beispiel:* +```php +$listeNr = 1029; // Beispiel-ID für ADR_Adressliste.repx +$pdfResponse = $pxrest->getList($listeNr); -//Zusatzinformationen zur letzten Response -$lastResponse = $pxrest->http->getResponse(); -$lastResponse->getCode(); // Response code (int). -$lastResponse->getHeaders(); // Response headers (array). -$lastResponse->getBody(); // Response body (JSON). +if ($pdfResponse->getCode() === 200) { + file_put_contents('Adressliste.pdf', $pdfResponse->getBody()); + echo "Liste erfolgreich als Adressliste.pdf gespeichert."; +} ``` +## Spezielle Endpunkte -#### Spezielle Endpunkte - +### Info -##### Info +Ruft Infos vom Endpunkt `PRO/Info` ab. -Ruft Infos vom Endpunkt **PRO/Info** ab. - -*Hinweis: Dieser Endpunkt / Abfrage blockiert keine Lizenz* +*Hinweis: Dieser Endpunkt / Abfrage blockiert keine Lizenz.* ```php -$pxrest = new Client(...) - -//Variante 1: API - Key direkt mitgeben +// Variante 1: API-Key direkt mitgeben $info1 = $pxrest->info('112a5a90fe28b23ed2c776562a7d1043957b5b79fad242b10141254b4de59028'); -//Variante 2: API - Key aus Options verwenden (sofern dort hinterlegt) +// Variante 2: API-Key aus Options verwenden (sofern dort hinterlegt) $info2 = $pxrest->info(); ``` -##### Datenbank +### Datenbank + +Ruft Infos vom Endpunkt `PRO/Datenbank` ab. + +```php +$dbInfo = $pxrest->database(); +``` + +## Response / Antwort -Ruft Infos vom Endpunkt **PRO/Datenbank** ab. +Alle Methoden geben die Response als Array bzw. `NULL` (z.B. bei `DELETE`) zurück. Bei Fehlern wird eine `HttpClientException` mit der Rückmeldung der PROFFIX REST-API geworfen. -*Hinweis: Dieser Endpunkt / Abfrage blockiert keine Lizenz* +Zudem lassen sich Zusatzinformationen zur letzten Response wie folgt ausgeben: + +### Letzter Request ```php -$pxrest = new Client(...); +$lastRequest = $pxrest->http->getRequest(); +$lastRequest->getUrl(); // Get requested URL (string). +$lastRequest->getMethod(); // Get request method (string). +$lastRequest->getParameters(); // Get request parameters (array). +$lastRequest->getHeaders(); // Get request headers (array). +$lastRequest->getBody(); // Get request body (JSON). +``` -//Variante 1: API - Key direkt mitgeben -$datenbank1 = $pxrest->database('112a5a90fe28b23ed2c776562a7d1043957b5b79fad242b10141254b4de59028'); - -//Variante 2: API - Key aus Options verwenden (sofern dort hinterlegt) -$datenbank2 = $pxrest->database(); - ``` -### Ausnahmen / Spezialfälle +### Letzte Response + +```php +$lastResponse = $pxrest->http->getResponse(); +$lastResponse->getCode(); // Response code (int). +$lastResponse->getHeaders(); // Response headers (array). +$lastResponse->getBody(); // Response body (JSON). +``` -* Endpunkte welche Leerschläge enthalten können (z.B. LAG/Artikel/PC 7/Bestand) müssen mit rawurlencode() genutzt werden +## Ausnahmen / Spezialfälle -### Weitere Beispiele +* Endpunkte, welche Leerschläge enthalten (z.B. `LAG/Artikel/PC 7/Bestand`), müssen mit `rawurlencode()` genutzt werden. -Im Ordner [/examples](https://github.com/pitwch/php-wrapper-proffix-restapi/tree/master/examples) finden sich weitere, -auskommentierte Beispiele. +## Weitere Beispiele +Im Ordner [/examples](https://github.com/pitwch/php-wrapper-proffix-restapi/tree/master/examples) finden sich weitere auskommentierte Beispiele. -# Weitere Wrapper für die Proffix Rest-API +## Weitere Wrapper für die Proffix Rest-API -- [Golang Wrapper für die Proffix Rest-API](https://github.com/pitwch/go-wrapper-proffix-restapi) :link: -- [Dart Wrapper für die Proffix Rest-API](https://github.com/pitwch/dart_proffix_rest) :link: +* [Golang Wrapper für die Proffix Rest-API](https://github.com/pitwch/go-wrapper-proffix-restapi) +* [Dart Wrapper für die Proffix Rest-API](https://github.com/pitwch/dart_proffix_rest) diff --git a/composer.json b/composer.json index 8c2d931..becca14 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "pitwch/rest-api-wrapper-proffix-php", "description": "PHP Wrapper for PROFFIX REST API", "type": "library", - "version": "1.9.1", + "version": "1.9.2", "homepage": "https://www.pitw.ch", "license": "MIT", "authors": [ @@ -28,8 +28,7 @@ }, "autoload-dev": { "psr-4": { - "Pitwch\\RestAPIWrapperProffix\\Tests\\": "tests/RestAPIWrapperProffix/", - "Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\": "tests/Integration/" + "Pitwch\\RestAPIWrapperProffix\\Tests\\": "tests/" } } } \ No newline at end of file diff --git a/composer.lock b/composer.lock index eb2eae2..14cc8fb 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a81d58b9c9ecae9af9c8abe08be6630e", + "content-hash": "11e0273ccd566225b44b7b413183f8ae", "packages": [], "packages-dev": [ { @@ -580,16 +580,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.23", + "version": "11.5.24", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "86ebcd8a3dbcd1857d88505109b2a2b376501cde" + "reference": "6b07ab1047155cf38f82dd691787a277782271dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/86ebcd8a3dbcd1857d88505109b2a2b376501cde", - "reference": "86ebcd8a3dbcd1857d88505109b2a2b376501cde", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6b07ab1047155cf38f82dd691787a277782271dd", + "reference": "6b07ab1047155cf38f82dd691787a277782271dd", "shasum": "" }, "require": { @@ -603,7 +603,7 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.2", - "phpunit/php-code-coverage": "^11.0.9", + "phpunit/php-code-coverage": "^11.0.10", "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-invoker": "^5.0.1", "phpunit/php-text-template": "^4.0.1", @@ -661,7 +661,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.23" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.24" }, "funding": [ { @@ -685,7 +685,7 @@ "type": "tidelift" } ], - "time": "2025-06-13T05:47:49+00:00" + "time": "2025-06-20T11:31:02+00:00" }, { "name": "sebastian/cli-parser", diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..811a987 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,19 @@ + + + + + ./tests/Integration + + + + + + + + + + diff --git a/src/RestAPIWrapperProffix/Client.php b/src/RestAPIWrapperProffix/Client.php index b057190..ff715aa 100644 --- a/src/RestAPIWrapperProffix/Client.php +++ b/src/RestAPIWrapperProffix/Client.php @@ -64,9 +64,12 @@ public function database($px_api_key = '') */ public function getList(int $listenr, array $body = []): \Pitwch\RestAPIWrapperProffix\HttpClient\Response { + // Per API documentation, a POST to /generieren requires a body, even if empty. + // It must be an empty JSON object `{}`. + $requestBody = empty($body) ? (object)[] : $body; + // First, send a POST request to generate the list file. - // The `post` method automatically handles JSON decoding and error checking. - $this->post('PRO/Liste/' . $listenr . '/generieren', $body); + $this->post('PRO/Liste/' . $listenr . '/generieren', $requestBody); // After a successful request, the HttpClient holds the last response. $postResponse = $this->getHttpClient()->getResponse(); diff --git a/src/RestAPIWrapperProffix/HttpClient/HttpClient.php b/src/RestAPIWrapperProffix/HttpClient/HttpClient.php index 9bb6855..9a9e50f 100644 --- a/src/RestAPIWrapperProffix/HttpClient/HttpClient.php +++ b/src/RestAPIWrapperProffix/HttpClient/HttpClient.php @@ -2,15 +2,11 @@ namespace Pitwch\RestAPIWrapperProffix\HttpClient; - -use Pitwch\RestAPIWrapperProffix\Client; use Pitwch\RestAPIWrapperProffix\HttpClient\HttpClientException; use Pitwch\RestAPIWrapperProffix\HttpClient\Options; use Pitwch\RestAPIWrapperProffix\HttpClient\Request; use Pitwch\RestAPIWrapperProffix\HttpClient\Response; - - /** * Class HttpClient * @@ -18,660 +14,326 @@ */ class HttpClient { - /** - * @var string The Proffix API URL - */ protected $url; - - /** - * @var string The Proffix API Database - */ protected $apiDatabase; - - /** - * @var array The Proffix API Modules - */ protected $apiModules; - - /** - * @var string The Proffix API User - */ protected $apiUser; - - /** - * @var string The Proffix API Password - */ protected $apiPassword; - - /** - * @var Options The options for the client - */ protected $options; - - /** - * @var Request The request object - */ public $request; - - /** - * @var Response The response object - */ public $response; - - /** - * @var string The Proffix Session ID - */ protected $pxSessionId; - - /** - * @var resource|\CurlHandle The cURL handle - */ private $ch; - - /** - * @var array The response headers - */ private $responseHeaders = []; - /** - * HttpClient constructor. - * @param $url - * @param $apiDatabase - * @param $apiUser - * @param $apiPassword - * @param $apiModules - * @param $options - * @throws HttpClientException - */ - /** - * HttpClient constructor. - * - * @param string $url The Proffix API URL - * @param string $apiDatabase The Proffix Database - * @param string $apiUser The Proffix User - * @param string $apiPassword The Proffix Password - * @param array $apiModules The required Proffix Modules - * @param array $options Additional options - * - * @throws HttpClientException - */ - public function __construct($url, $apiDatabase, $apiUser, $apiPassword, $apiModules, $options) + public function __construct($url, $apiDatabase, $apiUser, $apiPassword, $apiModules, $options = []) { - if (!\function_exists('curl_version')) { + if (!function_exists('curl_version')) { throw new HttpClientException('cURL is not installed on this server', -1, new Request(), new Response()); } + if (!is_array($options)) { + $options = []; + } + $this->options = new Options($options); $this->url = $this->buildApiUrl($url); $this->apiUser = $apiUser; $this->apiPassword = $apiPassword; $this->apiDatabase = $apiDatabase; $this->apiModules = $apiModules; - } - - /** - * Check if the connection is SSL - * - * @return bool - */ - protected function isSsl() - { - return 'https://' === \substr($this->url, 0, 8); - } - - - /** - * Build the API URL - * - * @param string $url - * - * @return string - */ protected function buildApiUrl($url) { - - return \rtrim($url, '/') . $this->options->apiPrefix() . $this->options->getVersion() . '/'; + return rtrim($url, '/') . $this->options->apiPrefix() . $this->options->getVersion() . '/'; } - - /** - * Build the URL with query parameters - * - * @param string $url - * @param array $parameters - * - * @return string - */ protected function buildUrlQuery($url, $parameters = []) { - //Create empty query - $query = array(); - if (!empty($parameters)) { - - //Check if we need to send ?key Param - if(array_key_exists('key',$parameters)){ - - //Check if Param Key is empty -> use from Options - if(empty($parameters['key'])){ - $parameters['key'] = $this->options->getApiKey(); - - } + if (array_key_exists('key', $parameters) && empty($parameters['key'])) { + $parameters['key'] = $this->options->getApiKey(); } - - $url .= '?'. \http_build_query($parameters); - + $url .= '?' . http_build_query($parameters); } - return $url; } - /** - * Build the JSON for the login request - * - * @return array - */ - protected function buildLoginJson() + protected function login() { - $loginJson = [ + $loginCredentials = [ 'Benutzer' => $this->apiUser, 'Passwort' => $this->apiPassword, 'Datenbank' => ['Name' => $this->apiDatabase], 'Module' => is_array($this->apiModules) ? $this->apiModules : explode(',', $this->apiModules) ]; - return $loginJson; - } - - /** - * Build the login URL - * - * @return string - */ - protected function buildLoginUrl() - { - - return \rtrim($this->url, '/') . '/' . $this->options->getLoginEndpoint(); - } + $loginUrl = rtrim($this->url, '/') . '/' . $this->options->getLoginEndpoint(); + $body = json_encode($loginCredentials); - /** - * Login to Proffix - * - * @return string The PxSessionId - * - * @throws HttpClientException - */ - protected function login() - { - // IMPORTANT: login() now uses the cURL handle initialized by request(). - // It will temporarily set its own options on this handle. - // Login has specific cURL needs, especially CURLOPT_HEADER = true. - - $body = \json_encode($this->buildLoginJson()); - $loginHeaders = [ + curl_setopt($this->ch, CURLOPT_URL, $loginUrl); + curl_setopt($this->ch, CURLOPT_POST, true); + curl_setopt($this->ch, CURLOPT_POSTFIELDS, $body); + curl_setopt($this->ch, CURLOPT_HEADER, true); + curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($this->ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', - 'Content-Length: ' . strlen($body), - 'Accept: application/json' // Good practice to include Accept for login too - ]; - - \curl_setopt($this->ch, CURLOPT_URL, $this->buildLoginUrl()); - \curl_setopt($this->ch, CURLOPT_CUSTOMREQUEST, 'POST'); - \curl_setopt($this->ch, CURLOPT_HTTPHEADER, $loginHeaders); - \curl_setopt($this->ch, CURLOPT_POSTFIELDS, $body); - \curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true); // Make sure login also returns response - \curl_setopt($this->ch, CURLOPT_HEADER, true); // Crucial for extracting PxSessionId from login response headers + 'Accept: application/json', + 'Content-Length: ' . strlen($body) + ]); $response = curl_exec($this->ch); + $headerSize = curl_getinfo($this->ch, CURLINFO_HEADER_SIZE); + $httpCode = curl_getinfo($this->ch, CURLINFO_HTTP_CODE); + $header = substr($response, 0, $headerSize); if (curl_errno($this->ch)) { - throw new HttpClientException('cURL error: ' . curl_error($this->ch), curl_errno($this->ch), $this->request, $this->response); + throw new HttpClientException('cURL error during login: ' . curl_error($this->ch), curl_errno($this->ch), $this->request, null); } - $headerSize = curl_getinfo($this->ch, CURLINFO_HEADER_SIZE); - $header = substr($response, 0, $headerSize); - $this->pxSessionId = $this->extractSessionId($header); - + if (empty($this->pxSessionId)) { $responseBody = substr($response, $headerSize); $parsedBody = json_decode($responseBody); - $errorMessage = 'Failed to retrieve PxSessionId from login response.'; - if (isset($parsedBody->Message)) { - $errorMessage .= ' Proffix API Error: ' . $parsedBody->Message; - } - throw new HttpClientException($errorMessage, 401, $this->request, $this->response); + $errorMessage = $parsedBody->Message ?? 'Login failed: PxSessionId not found in response headers.'; + throw new HttpClientException($errorMessage, $httpCode, $this->request, null); } - - return $this->pxSessionId; } - /** - * Logout from Proffix - * - * @return bool - * - * @throws HttpClientException - */ protected function logout() { if (empty($this->pxSessionId)) { - return true; + return; } - $this->initCurl(); - - $headers = [ - 'PxSessionId: ' . $this->pxSessionId - ]; - - curl_setopt($this->ch, CURLOPT_URL, $this->buildLoginUrl()); - curl_setopt($this->ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); - curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers); + $ch = curl_init(); + $logoutUrl = rtrim($this->url, '/') . '/' . $this->options->getLoginEndpoint(); + $headers = $this->getRequestHeaders(false); + $rawHeaders = []; + foreach ($headers as $key => $value) { + $rawHeaders[] = $key . ': ' . $value; + } - curl_exec($this->ch); + curl_setopt($ch, CURLOPT_URL, $logoutUrl); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); + curl_setopt($ch, CURLOPT_HTTPHEADER, $rawHeaders); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false); + curl_setopt($ch, CURLOPT_TIMEOUT, 5); // Use a short timeout - if (curl_errno($this->ch)) { - throw new HttpClientException('cURL error on logout: ' . curl_error($this->ch), curl_errno($this->ch), $this->request, $this->response); - } + curl_exec($ch); + curl_close($ch); $this->pxSessionId = null; - return true; } - /** - * Set the HTTP method for the cURL request - * - * @param string $method - */ - protected function setupMethod($method) - { - // Reset method-specific options first to ensure a clean state - \curl_setopt($this->ch, CURLOPT_HTTPGET, false); - \curl_setopt($this->ch, CURLOPT_POST, false); - \curl_setopt($this->ch, CURLOPT_CUSTOMREQUEST, null); // Reset custom request - - if ('GET' == $method) { - \curl_setopt($this->ch, CURLOPT_HTTPGET, true); - } elseif ('POST' == $method) { - \curl_setopt($this->ch, CURLOPT_POST, true); - } elseif (\in_array($method, ['PUT', 'DELETE', 'OPTIONS'])) { - \curl_setopt($this->ch, CURLOPT_CUSTOMREQUEST, $method); - } - // For POST, PUT, DELETE with body, CURLOPT_POSTFIELDS will be set later in request() - } - /** - * Get the request headers - * - * @param bool $sendData - * - * @return array - * - * @throws HttpClientException - */ - protected function getRequestHeaders($sendData = false) + + protected function getRequestHeaders($sendData = false, $body = '') { $headers = [ 'Accept' => 'application/json', - 'User-Agent' => $this->options->userAgent() . '/' . Options::VERSION, + 'User-Agent' => $this->options->userAgent() . '/' . Options::VERSION, ]; - if (!empty($this->pxSessionId)) { $headers['PxSessionId'] = $this->pxSessionId; } - if ($sendData) { $headers['Content-Type'] = 'application/json;charset=utf-8'; + $headers['Content-Length'] = strlen($body); } - return $headers; } - /** - * Create the request object - * - * @param string $endpoint - * @param string $method - * @param array $data - * @param array $parameters - * - * @return Request - * - * @throws HttpClientException - */ protected function createRequest($endpoint, $method, $data = [], $parameters = []) { - $body = ''; $url = $this->url . $endpoint; - $hasData = !empty($data); - - if ($hasData) { - $body = \json_encode($data); - } - + $body = json_encode($data); $this->request = new Request( $this->buildUrlQuery($url, $parameters), $method, $parameters, - $this->getRequestHeaders($hasData), + $this->getRequestHeaders(!empty($data), $body), $body ); - - return $this->request; } - /** - * Get the response headers - * - * @return array - */ protected function getResponseHeaders() { $headers = []; - $lines = explode("\n", (string)$this->responseHeaders); - $lines = \array_filter($lines, 'trim'); - - foreach ($lines as $index => $line) { - // Remove HTTP/xxx params. - if (strpos($line, ': ') === false) { - continue; + $lines = explode("\n", trim((string)$this->responseHeaders)); + foreach ($lines as $line) { + if (strpos($line, ': ') !== false) { + list($key, $value) = explode(': ', $line, 2); + $headers[trim($key)] = trim($value); } - - list($key, $value) = \explode(': ', $line); - - $headers[$key] = isset($headers[$key]) ? $headers[$key] . ', ' . trim($value) : trim($value); } - return $headers; } - /** - * Create the response object - * - * @return Response - * - * @throws HttpClientException - */ protected function createResponse() { $this->response = new Response(); } - /** - * Set the default cURL settings - */ protected function setDefaultCurlSettings() { $verifySsl = $this->options->verifySsl(); - $timeout = $this->options->getTimeout(); - $followRedirects = $this->options->getFollowRedirects(); - - \curl_setopt($this->ch, CURLOPT_SSL_VERIFYPEER, $verifySsl); + curl_setopt($this->ch, CURLOPT_SSL_VERIFYPEER, $verifySsl); if (!$verifySsl) { - \curl_setopt($this->ch, CURLOPT_SSL_VERIFYHOST, $verifySsl); - } - if ($followRedirects) { - \curl_setopt($this->ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($this->ch, CURLOPT_SSL_VERIFYHOST, $verifySsl); } - \curl_setopt($this->ch, CURLOPT_CONNECTTIMEOUT, $timeout); - \curl_setopt($this->ch, CURLOPT_TIMEOUT, $timeout); - \curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true); - \curl_setopt($this->ch, CURLOPT_HEADER, false); // Default for API calls: no headers in body - // Note: CURLOPT_HTTPHEADER is NOT set here anymore. It's set in request() method. - \curl_setopt($this->ch, CURLOPT_URL, $this->request->getUrl()); + curl_setopt($this->ch, CURLOPT_TIMEOUT, $this->options->getTimeout()); + curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($this->ch, CURLOPT_HEADER, false); + curl_setopt($this->ch, CURLOPT_URL, $this->request->getUrl()); } - /** - * Check for errors in the response - * - * @param mixed $parsedResponse - * - * @throws HttpClientException - */ protected function lookForErrors($parsedResponse) { - // Any non-200/201/202/204 response code indicates an error. - if (!in_array($this->response->getCode(), ['200', '201', '202', '204'])) { + if (!in_array($this->response->getCode(), [200, 201, 202, 204])) { $errorMessage = 'An unknown error occurred'; - if (isset($parsedResponse->Message)) { $errorMessage = $parsedResponse->Message; - } elseif (is_string($parsedResponse)) { - $errorMessage = $parsedResponse; } - - if (isset($parsedResponse->Fields) && is_array($parsedResponse->Fields)) { - $fieldErrors = []; - foreach ($parsedResponse->Fields as $field) { - $fieldErrors[] = sprintf( - 'Field: %s (%s) - %s', - $field->Name ?? 'N/A', - $field->Reason ?? 'N/A', - $field->Message ?? 'N/A' - ); - } - if (!empty($fieldErrors)) { - $errorMessage .= ': ' . implode('; ', $fieldErrors); - } - } - - throw new HttpClientException( - sprintf('Error: %s', $errorMessage), - $this->response->getCode(), - $this->request, - $this->response - ); - } - } - /** - * @param $errors - * @return array - */ - protected function parsePxErrorMessage($errors) - { - foreach ($errors as $error){ - $clean[] = $error; + throw new HttpClientException($errorMessage, $this->response->getCode(), $this->request, $this->response); } } - /** - * Process the response - * - * @return mixed - * - * @throws HttpClientException - */ + protected function processResponse() { $body = $this->response->getBody(); - - $parsedResponse = \json_decode($body); - - // Test if return a valid JSON. - if (JSON_ERROR_NONE !== json_last_error() && ($this->response->getCode() != 201 && $this->response->getCode() != 204)) { - $message = function_exists('json_last_error_msg') ? json_last_error_msg() : 'Invalid JSON returned'; - throw new HttpClientException($message, $this->response->getCode(), $this->request, $this->response); + $parsedResponse = json_decode($body); + if (json_last_error() !== JSON_ERROR_NONE && !in_array($this->response->getCode(), [201, 204])) { + throw new HttpClientException(json_last_error_msg(), $this->response->getCode(), $this->request, $this->response); } - $this->lookForErrors($parsedResponse); - return $parsedResponse; } - - public function request($endpoint, $method, $data = [], $parameters = [], $login = true) { $this->prepareRequest($endpoint, $method, $data, $parameters, $login); return $this->executeCurl(true); } - /** - * @param $endpoint - * @param $method - * @param array $data - * @param array $parameters - * @return Response - * @throws HttpClientException - */ public function rawRequest($endpoint, $method, $data = [], $parameters = []): Response { - $this->prepareRequest($endpoint, $method, $data, $parameters, true); // Login is always required for raw requests + $this->prepareRequest($endpoint, $method, $data, $parameters, true); return $this->executeCurl(false); } - /** - * @param $endpoint - * @param $method - * @param $data - * @param $parameters - * @param $login - * @throws HttpClientException - */ private function prepareRequest($endpoint, $method, $data, $parameters, $login) { $this->initCurl(); + $body = json_encode($data); + $hasData = ($body !== '[]' && $body !== '{}') || !in_array($method, ['GET', 'DELETE']); - // Create the request object for the main operation. $this->createRequest($endpoint, $method, $data, $parameters); - // If login is required, it's performed first. if ($login && empty($this->pxSessionId)) { $this->login(); } - // Apply default cURL settings for the MAIN request. + // Always set the default settings for the actual request, AFTER a potential login $this->setDefaultCurlSettings(); - // Get the final headers for this specific request (which will include PxSessionId if login occurred) - $finalRequestHeaders = $this->getRequestHeaders(!empty($data)); - $rawFinalRequestHeaders = []; - foreach ($finalRequestHeaders as $key => $value) { - $rawFinalRequestHeaders[] = $key . ': ' . $value; + // Reset all relevant cURL options to a clean state before every request + curl_setopt($this->ch, CURLOPT_HTTPGET, false); + curl_setopt($this->ch, CURLOPT_POST, false); + curl_setopt($this->ch, CURLOPT_CUSTOMREQUEST, null); + curl_setopt($this->ch, CURLOPT_POSTFIELDS, null); + + // Explicitly configure the handle for the current request + switch ($method) { + case 'POST': + curl_setopt($this->ch, CURLOPT_POST, true); + curl_setopt($this->ch, CURLOPT_POSTFIELDS, $body); + break; + case 'GET': + curl_setopt($this->ch, CURLOPT_HTTPGET, true); + break; + case 'PUT': + curl_setopt($this->ch, CURLOPT_CUSTOMREQUEST, 'PUT'); + curl_setopt($this->ch, CURLOPT_POSTFIELDS, $body); + break; + case 'DELETE': + curl_setopt($this->ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); + break; + default: + curl_setopt($this->ch, CURLOPT_CUSTOMREQUEST, $method); + break; } - \curl_setopt($this->ch, CURLOPT_HTTPHEADER, $rawFinalRequestHeaders); - // Setup method. - $this->setupMethod($method); - - // Include post fields. - if (!empty($data)) { - $body = \json_encode($data); - \curl_setopt($this->ch, CURLOPT_POSTFIELDS, $body); + $rawHeaders = []; + foreach ($this->getRequestHeaders($hasData, $body) as $key => $value) { + $rawHeaders[] = $key . ': ' . $value; } - - $this->createResponse(); + curl_setopt($this->ch, CURLOPT_HTTPHEADER, $rawHeaders); } - /** - * @param bool $processJson - * @return mixed|Response - * @throws HttpClientException - */ private function executeCurl(bool $processJson = true) { - // Set response headers callback $this->responseHeaders = ''; - \curl_setopt($this->ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) { + curl_setopt($this->ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) { $this->responseHeaders .= $header; return strlen($header); }); + $this->createResponse(); $body = curl_exec($this->ch); if (curl_errno($this->ch)) { - throw new HttpClientException('cURL error: ' . curl_error($this->ch), curl_errno($this->ch), $this->request, $this->response); + $error_msg = curl_error($this->ch); + $error_no = curl_errno($this->ch); + curl_close($this->ch); + $this->ch = null; + throw new HttpClientException('cURL error: ' . $error_msg, $error_no, $this->request, $this->response); } $this->response->setBody($body); $this->response->setCode(curl_getinfo($this->ch, CURLINFO_HTTP_CODE)); $this->response->setHeaders($this->getResponseHeaders()); + curl_close($this->ch); + $this->ch = null; + if ($processJson) { - // processResponse will decode and also call lookForErrors return $this->processResponse(); } - // For raw requests, we only check for non-2xx status codes. - if (!in_array($this->response->getCode(), ['200', '201', '202', '204'])) { - // Try to parse the body as JSON to get a detailed error message - $parsedError = \json_decode($this->response->getBody()); - if (JSON_ERROR_NONE === json_last_error()) { - // It's a JSON error response, pass it to lookForErrors - $this->lookForErrors($parsedError); - } else { - // Not a JSON error, create a generic exception - throw new HttpClientException( - 'HTTP Error ' . $this->response->getCode(), - $this->response->getCode(), - $this->request, - $this->response - ); - } - } - + $this->lookForErrors(null); return $this->response; } - /** - * Get the request object - * - * @return Request - */ - public function getRequest() - { - return $this->request; - } + public function getRequest() { return $this->request; } + public function getResponse() { return $this->response; } - /** - * Get the response object - * - * @return Response - */ - public function getResponse() - { - return $this->response; - } - - /** - * Initialize the cURL handle - */ private function initCurl() { - if (!$this->ch) { - $this->ch = curl_init(); - } + // Always create a new cURL handle to ensure a stateless request + $this->ch = curl_init(); } - /** - * Extract the session ID from the response headers - * - * @param string $header - * - * @return string|null - */ - private function extractSessionId($header) + private function extractSessionId($headerBlock) { - foreach (explode("\r\n", $header) as $line) { - if (strpos($line, 'PxSessionId:') === 0) { - return trim(substr($line, strlen('PxSessionId:'))); + $lines = explode("\n", $headerBlock); + foreach ($lines as $line) { + if (stripos($line, 'PxSessionId:') === 0) { + list(, $value) = explode(':', $line, 2); + return trim($value); } } - return null; } - /** - * Destructor - */ public function __destruct() { - if ($this->ch) { - if(!empty($this->pxSessionId)){ - $this->logout(); - } - curl_close($this->ch); + // Attempt to log out if a session was active. + // The logout method is now self-contained and will handle its own cURL session. + if (!empty($this->pxSessionId)) { + $this->logout(); } } } diff --git a/tests/.phpunit.result.cache b/tests/.phpunit.result.cache deleted file mode 100644 index 7306b06..0000000 --- a/tests/.phpunit.result.cache +++ /dev/null @@ -1 +0,0 @@ -{"version":1,"defects":{"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\ClientIntegrationTest::testCanGetAddressList":8,"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\ClientIntegrationTest::testCanGetArticleStock":7,"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\ClientIntegrationTest::testCanCreateAddress":8,"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\ClientIntegrationTest::testCanUpdateNewlyCreatedAddress":8,"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\ClientIntegrationTest::testCanGetCountryDetailsCH":8,"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\ClientIntegrationTest::testCanDeleteAddress":8},"times":{"Pitwch\\RestAPIWrapperProffix\\Tests\\HttpClientTest::testHttpClientCanBeInstantiated":0.003,"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\ClientIntegrationTest::testClientCanBeInstantiated":0,"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\ClientIntegrationTest::testCanGetAddressList":0.011,"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\ClientIntegrationTest::testCanGetArticleStock":2.475,"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\MinimalIntegrationTest::testMinimalAssertion":0.001,"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\AppleTest::testRedDelicious":0.003,"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\ClientIntegrationTest::testCanGetCountryDetailsCH":0.001,"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\ClientIntegrationTest::testCanCreateAddress":0.001,"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\ClientIntegrationTest::testCanUpdateNewlyCreatedAddress":0.001,"Pitwch\\RestAPIWrapperProffix\\Tests\\Integration\\ClientIntegrationTest::testCanDeleteAddress":0.001}} \ No newline at end of file diff --git a/tests/Integration/ClientIntegrationTest.php b/tests/Integration/ClientIntegrationTest.php index fc7eab2..13f7d25 100644 --- a/tests/Integration/ClientIntegrationTest.php +++ b/tests/Integration/ClientIntegrationTest.php @@ -19,12 +19,14 @@ protected function setUp(): void $this->client = new Client( $_ENV['PROFFIX_API_URL'], $_ENV['PROFFIX_API_DATABASE'], - $_ENV['PROFFIX_API_USER'], + $_ENV['PROFFIX_API_USERNAME'], $_ENV['PROFFIX_API_PASSWORD'], - $_ENV['PROFFIX_API_MODULES'] + $_ENV['PROFFIX_API_MODULES'], + [] ); } + public function testCanGetAddressList(): void { $addresses = $this->client->get('ADR/Adresse'); @@ -158,8 +160,22 @@ public function testCanDeleteAddress(): void public function testCanGetList(): void { + // 1. Find the list number dynamically + $listName = 'ADR_Adressliste.repx'; + $listInfo = $this->client->get('PRO/Liste', [ + 'Filter' => "Name=='{$listName}'", + 'limit' => 1, + 'fields' => 'ListeNr' + ]); + + if (empty($listInfo) || !isset($listInfo[0]->ListeNr)) { + $this->markTestSkipped("List '{$listName}' not found. Skipping getList test."); + } + $listeNr = $listInfo[0]->ListeNr; + try { - $response = $this->client->getList(1029); // Using a known list ID from Go tests + // 2. Get the list using the found ID + $response = $this->client->getList($listeNr); $this->assertEquals(200, $response->getCode()); $this->assertNotEmpty($response->getBody()); @@ -171,7 +187,7 @@ public function testCanGetList(): void // The list might not exist in all test environments. If so, skip the test. // A 404 on the final GET will be caught here. if ($e->getCode() === 404) { - $this->markTestSkipped('List with ID 1029 not found or failed to generate. Skipping getList test.'); + $this->markTestSkipped("List with ID {$listeNr} not found or failed to generate. Skipping getList test."); } else { // Re-throw other exceptions throw $e; diff --git a/tests/RestAPIWrapperProffix/HttpClientTest.php b/tests/RestAPIWrapperProffix/HttpClientTest.php deleted file mode 100644 index d66faaa..0000000 --- a/tests/RestAPIWrapperProffix/HttpClientTest.php +++ /dev/null @@ -1,44 +0,0 @@ -defaultOptions = [ - 'url' => 'http://fake-api.proffix.com', - 'apiDatabase' => 'FAKEDB', - 'apiUser' => 'testuser', - 'apiPassword' => 'password', - 'apiModules' => ['CRM', 'ADR'], - 'options' => [] // Default HttpClient Options - ]; - - $this->httpClient = new HttpClient( - $this->defaultOptions['url'], - $this->defaultOptions['apiDatabase'], - $this->defaultOptions['apiUser'], - $this->defaultOptions['apiPassword'], - $this->defaultOptions['apiModules'], - $this->defaultOptions['options'] - ); - } - - public function testHttpClientCanBeInstantiated(): void - { - $this->assertInstanceOf(HttpClient::class, $this->httpClient); - } - - // More tests will be added here, e.g., for login, request methods, error handling. - // These will likely require mocking cURL functions or HTTP responses. - -} diff --git a/tests/phpunit.xml b/tests/phpunit.xml deleted file mode 100644 index b07d758..0000000 --- a/tests/phpunit.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - ./Integration - - - -