From d5273f2170dfaf54fb7dbb60f2f448d5d434e4a6 Mon Sep 17 00:00:00 2001 From: SP Date: Tue, 24 Jun 2025 17:43:09 +0200 Subject: [PATCH] Fix: Refactor, add getList --- composer.json | 2 +- src/RestAPIWrapperProffix/Client.php | 45 +++++++ .../HttpClient/HttpClient.php | 124 ++++++++++++------ tests/Integration/ClientIntegrationTest.php | 23 ++++ 4 files changed, 156 insertions(+), 38 deletions(-) diff --git a/composer.json b/composer.json index f1facbb..8c2d931 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.0", + "version": "1.9.1", "homepage": "https://www.pitw.ch", "license": "MIT", "authors": [ diff --git a/src/RestAPIWrapperProffix/Client.php b/src/RestAPIWrapperProffix/Client.php index 4f30ed5..b057190 100644 --- a/src/RestAPIWrapperProffix/Client.php +++ b/src/RestAPIWrapperProffix/Client.php @@ -3,6 +3,7 @@ namespace Pitwch\RestAPIWrapperProffix; use Pitwch\RestAPIWrapperProffix\HttpClient\HttpClient; +use Pitwch\RestAPIWrapperProffix\HttpClient\HttpClientException; /** * Class Client @@ -52,5 +53,49 @@ public function database($px_api_key = '') { return $this->httpClient->request('PRO/Datenbank', 'GET', [], ['key' => $px_api_key], false); } + + /** + * Generates a list and returns the file content. + * + * @param int $listenr The ID of the list to generate. + * @param array $body The request body for generating the list. + * @return array An array containing the response body, headers, and status code of the file download. + * @throws HttpClientException + */ + public function getList(int $listenr, array $body = []): \Pitwch\RestAPIWrapperProffix\HttpClient\Response + { + // 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); + + // After a successful request, the HttpClient holds the last response. + $postResponse = $this->getHttpClient()->getResponse(); + + // The API returns 201 Created on success, which is already validated by lookForErrors. + // We just need to get the Location header. + $postHeaders = $postResponse->getHeaders(); + if (!isset($postHeaders['Location'])) { + throw new HttpClientException('Location header not found in response for list generation.', 404, $this->getHttpClient()->getRequest(), $postResponse); + } + + // Extract the file ID from the Location header + $dateiNr = $this->convertLocationToId($postHeaders['Location']); + + // Use the new `rawRequest` method to download the file. + // This method returns a Response object directly, without trying to parse the body as JSON. + return $this->httpClient->rawRequest('PRO/Datei/' . $dateiNr, 'GET'); + } + + /** + * Extracts the file ID from the Location header URL. + * e.g. /v4/PRO/Datei/12345 -> 12345 + * + * @param string $location + * @return string + */ + private function convertLocationToId(string $location): string + { + return basename($location); + } } diff --git a/src/RestAPIWrapperProffix/HttpClient/HttpClient.php b/src/RestAPIWrapperProffix/HttpClient/HttpClient.php index e684b7c..9bb6855 100644 --- a/src/RestAPIWrapperProffix/HttpClient/HttpClient.php +++ b/src/RestAPIWrapperProffix/HttpClient/HttpClient.php @@ -235,7 +235,13 @@ protected function login() $this->pxSessionId = $this->extractSessionId($header); if (empty($this->pxSessionId)) { - throw new HttpClientException('Failed to retrieve PxSessionId from login response.', 401, $this->request, $this->response); + $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); } return $this->pxSessionId; @@ -390,23 +396,7 @@ protected function getResponseHeaders() */ protected function createResponse() { - - // Set response headers. - $this->responseHeaders = ''; - \curl_setopt($this->ch, CURLOPT_HEADERFUNCTION, function ($_, $headers) { - $this->responseHeaders .= $headers; - return \strlen($headers); - }); - - // Get response data. - $body = \curl_exec($this->ch); - $code = \curl_getinfo($this->ch, CURLINFO_HTTP_CODE); - $headers = $this->getResponseHeaders(); - - // Register response. - $this->response = new Response($code, $headers, $body); - - return $this->getResponse(); + $this->response = new Response(); } /** @@ -509,43 +499,58 @@ protected function processResponse() 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 mixed + * @return Response * @throws HttpClientException */ - public function request($endpoint, $method, $data = [], $parameters = [], $login = true) + public function rawRequest($endpoint, $method, $data = [], $parameters = []): Response + { + $this->prepareRequest($endpoint, $method, $data, $parameters, true); // Login is always required for raw requests + 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(); // Create the request object for the main operation. - // This will be available if login() throws an exception. $this->createRequest($endpoint, $method, $data, $parameters); - // If login is required, it's performed first. - // login() will use the current $this->ch, temporarily setting options for the login call. + // If login is required, it's performed first. if ($login && empty($this->pxSessionId)) { - $this->login(); // This populates $this->pxSessionId if successful + $this->login(); } - // Now, apply default cURL settings for the MAIN request. - // This sets the main request's URL (from $this->request), CURLOPT_HEADER=false, etc., - // effectively overriding any temporary settings login() might have applied to $this->ch. - $this->setDefaultCurlSettings(); - - // Clear any headers from a potential previous login call on the same handle - \curl_setopt($this->ch, CURLOPT_HTTPHEADER, []); + // Apply default cURL settings for the MAIN request. + $this->setDefaultCurlSettings(); - // Now, get the final headers for this specific request (which will include PxSessionId if login occurred) + // 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; } - \curl_setopt($this->ch, CURLOPT_HTTPHEADER, $rawFinalRequestHeaders); // Set the final headers for the main API call + \curl_setopt($this->ch, CURLOPT_HTTPHEADER, $rawFinalRequestHeaders); // Setup method. $this->setupMethod($method); @@ -557,11 +562,56 @@ public function request($endpoint, $method, $data = [], $parameters = [], $login } $this->createResponse(); - // Process response once and store it - $processedResponse = $this->processResponse(); - $this->lookForErrors($processedResponse); + } + + /** + * @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) { + $this->responseHeaders .= $header; + return strlen($header); + }); + + $body = curl_exec($this->ch); - return $processedResponse; + if (curl_errno($this->ch)) { + throw new HttpClientException('cURL error: ' . curl_error($this->ch), curl_errno($this->ch), $this->request, $this->response); + } + + $this->response->setBody($body); + $this->response->setCode(curl_getinfo($this->ch, CURLINFO_HTTP_CODE)); + $this->response->setHeaders($this->getResponseHeaders()); + + 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 + ); + } + } + + return $this->response; } /** diff --git a/tests/Integration/ClientIntegrationTest.php b/tests/Integration/ClientIntegrationTest.php index a676227..fc7eab2 100644 --- a/tests/Integration/ClientIntegrationTest.php +++ b/tests/Integration/ClientIntegrationTest.php @@ -155,4 +155,27 @@ public function testCanDeleteAddress(): void $getResponse = $this->client->get('ADR/Adresse/' . $addressId); $this->assertTrue($getResponse->Geloescht); } + + public function testCanGetList(): void + { + try { + $response = $this->client->getList(1029); // Using a known list ID from Go tests + + $this->assertEquals(200, $response->getCode()); + $this->assertNotEmpty($response->getBody()); + $headers = $response->getHeaders(); + $this->assertArrayHasKey('Content-Type', $headers); + $this->assertEquals('application/pdf', $headers['Content-Type']); + + } catch (HttpClientException $e) { + // 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.'); + } else { + // Re-throw other exceptions + throw $e; + } + } + } }