Skip to content

Commit 8b82e00

Browse files
authored
Merge pull request #9 from pitwch/v2
Fix: Refactor, add getList
2 parents acde769 + d5273f2 commit 8b82e00

File tree

4 files changed

+156
-38
lines changed

4 files changed

+156
-38
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "pitwch/rest-api-wrapper-proffix-php",
33
"description": "PHP Wrapper for PROFFIX REST API",
44
"type": "library",
5-
"version": "1.9.0",
5+
"version": "1.9.1",
66
"homepage": "https://www.pitw.ch",
77
"license": "MIT",
88
"authors": [

src/RestAPIWrapperProffix/Client.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Pitwch\RestAPIWrapperProffix;
44

55
use Pitwch\RestAPIWrapperProffix\HttpClient\HttpClient;
6+
use Pitwch\RestAPIWrapperProffix\HttpClient\HttpClientException;
67

78
/**
89
* Class Client
@@ -52,5 +53,49 @@ public function database($px_api_key = '')
5253
{
5354
return $this->httpClient->request('PRO/Datenbank', 'GET', [], ['key' => $px_api_key], false);
5455
}
56+
57+
/**
58+
* Generates a list and returns the file content.
59+
*
60+
* @param int $listenr The ID of the list to generate.
61+
* @param array $body The request body for generating the list.
62+
* @return array An array containing the response body, headers, and status code of the file download.
63+
* @throws HttpClientException
64+
*/
65+
public function getList(int $listenr, array $body = []): \Pitwch\RestAPIWrapperProffix\HttpClient\Response
66+
{
67+
// First, send a POST request to generate the list file.
68+
// The `post` method automatically handles JSON decoding and error checking.
69+
$this->post('PRO/Liste/' . $listenr . '/generieren', $body);
70+
71+
// After a successful request, the HttpClient holds the last response.
72+
$postResponse = $this->getHttpClient()->getResponse();
73+
74+
// The API returns 201 Created on success, which is already validated by lookForErrors.
75+
// We just need to get the Location header.
76+
$postHeaders = $postResponse->getHeaders();
77+
if (!isset($postHeaders['Location'])) {
78+
throw new HttpClientException('Location header not found in response for list generation.', 404, $this->getHttpClient()->getRequest(), $postResponse);
79+
}
80+
81+
// Extract the file ID from the Location header
82+
$dateiNr = $this->convertLocationToId($postHeaders['Location']);
83+
84+
// Use the new `rawRequest` method to download the file.
85+
// This method returns a Response object directly, without trying to parse the body as JSON.
86+
return $this->httpClient->rawRequest('PRO/Datei/' . $dateiNr, 'GET');
87+
}
88+
89+
/**
90+
* Extracts the file ID from the Location header URL.
91+
* e.g. /v4/PRO/Datei/12345 -> 12345
92+
*
93+
* @param string $location
94+
* @return string
95+
*/
96+
private function convertLocationToId(string $location): string
97+
{
98+
return basename($location);
99+
}
55100
}
56101

src/RestAPIWrapperProffix/HttpClient/HttpClient.php

Lines changed: 87 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,13 @@ protected function login()
235235
$this->pxSessionId = $this->extractSessionId($header);
236236

237237
if (empty($this->pxSessionId)) {
238-
throw new HttpClientException('Failed to retrieve PxSessionId from login response.', 401, $this->request, $this->response);
238+
$responseBody = substr($response, $headerSize);
239+
$parsedBody = json_decode($responseBody);
240+
$errorMessage = 'Failed to retrieve PxSessionId from login response.';
241+
if (isset($parsedBody->Message)) {
242+
$errorMessage .= ' Proffix API Error: ' . $parsedBody->Message;
243+
}
244+
throw new HttpClientException($errorMessage, 401, $this->request, $this->response);
239245
}
240246

241247
return $this->pxSessionId;
@@ -390,23 +396,7 @@ protected function getResponseHeaders()
390396
*/
391397
protected function createResponse()
392398
{
393-
394-
// Set response headers.
395-
$this->responseHeaders = '';
396-
\curl_setopt($this->ch, CURLOPT_HEADERFUNCTION, function ($_, $headers) {
397-
$this->responseHeaders .= $headers;
398-
return \strlen($headers);
399-
});
400-
401-
// Get response data.
402-
$body = \curl_exec($this->ch);
403-
$code = \curl_getinfo($this->ch, CURLINFO_HTTP_CODE);
404-
$headers = $this->getResponseHeaders();
405-
406-
// Register response.
407-
$this->response = new Response($code, $headers, $body);
408-
409-
return $this->getResponse();
399+
$this->response = new Response();
410400
}
411401

412402
/**
@@ -509,43 +499,58 @@ protected function processResponse()
509499
return $parsedResponse;
510500
}
511501

502+
503+
504+
public function request($endpoint, $method, $data = [], $parameters = [], $login = true)
505+
{
506+
$this->prepareRequest($endpoint, $method, $data, $parameters, $login);
507+
return $this->executeCurl(true);
508+
}
509+
512510
/**
513511
* @param $endpoint
514512
* @param $method
515513
* @param array $data
516514
* @param array $parameters
517-
* @return mixed
515+
* @return Response
518516
* @throws HttpClientException
519517
*/
520-
public function request($endpoint, $method, $data = [], $parameters = [], $login = true)
518+
public function rawRequest($endpoint, $method, $data = [], $parameters = []): Response
519+
{
520+
$this->prepareRequest($endpoint, $method, $data, $parameters, true); // Login is always required for raw requests
521+
return $this->executeCurl(false);
522+
}
523+
524+
/**
525+
* @param $endpoint
526+
* @param $method
527+
* @param $data
528+
* @param $parameters
529+
* @param $login
530+
* @throws HttpClientException
531+
*/
532+
private function prepareRequest($endpoint, $method, $data, $parameters, $login)
521533
{
522534
$this->initCurl();
523535

524536
// Create the request object for the main operation.
525-
// This will be available if login() throws an exception.
526537
$this->createRequest($endpoint, $method, $data, $parameters);
527538

528-
// If login is required, it's performed first.
529-
// login() will use the current $this->ch, temporarily setting options for the login call.
539+
// If login is required, it's performed first.
530540
if ($login && empty($this->pxSessionId)) {
531-
$this->login(); // This populates $this->pxSessionId if successful
541+
$this->login();
532542
}
533543

534-
// Now, apply default cURL settings for the MAIN request.
535-
// This sets the main request's URL (from $this->request), CURLOPT_HEADER=false, etc.,
536-
// effectively overriding any temporary settings login() might have applied to $this->ch.
537-
$this->setDefaultCurlSettings();
538-
539-
// Clear any headers from a potential previous login call on the same handle
540-
\curl_setopt($this->ch, CURLOPT_HTTPHEADER, []);
544+
// Apply default cURL settings for the MAIN request.
545+
$this->setDefaultCurlSettings();
541546

542-
// Now, get the final headers for this specific request (which will include PxSessionId if login occurred)
547+
// Get the final headers for this specific request (which will include PxSessionId if login occurred)
543548
$finalRequestHeaders = $this->getRequestHeaders(!empty($data));
544549
$rawFinalRequestHeaders = [];
545550
foreach ($finalRequestHeaders as $key => $value) {
546551
$rawFinalRequestHeaders[] = $key . ': ' . $value;
547552
}
548-
\curl_setopt($this->ch, CURLOPT_HTTPHEADER, $rawFinalRequestHeaders); // Set the final headers for the main API call
553+
\curl_setopt($this->ch, CURLOPT_HTTPHEADER, $rawFinalRequestHeaders);
549554

550555
// Setup method.
551556
$this->setupMethod($method);
@@ -557,11 +562,56 @@ public function request($endpoint, $method, $data = [], $parameters = [], $login
557562
}
558563

559564
$this->createResponse();
560-
// Process response once and store it
561-
$processedResponse = $this->processResponse();
562-
$this->lookForErrors($processedResponse);
565+
}
566+
567+
/**
568+
* @param bool $processJson
569+
* @return mixed|Response
570+
* @throws HttpClientException
571+
*/
572+
private function executeCurl(bool $processJson = true)
573+
{
574+
// Set response headers callback
575+
$this->responseHeaders = '';
576+
\curl_setopt($this->ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) {
577+
$this->responseHeaders .= $header;
578+
return strlen($header);
579+
});
580+
581+
$body = curl_exec($this->ch);
563582

564-
return $processedResponse;
583+
if (curl_errno($this->ch)) {
584+
throw new HttpClientException('cURL error: ' . curl_error($this->ch), curl_errno($this->ch), $this->request, $this->response);
585+
}
586+
587+
$this->response->setBody($body);
588+
$this->response->setCode(curl_getinfo($this->ch, CURLINFO_HTTP_CODE));
589+
$this->response->setHeaders($this->getResponseHeaders());
590+
591+
if ($processJson) {
592+
// processResponse will decode and also call lookForErrors
593+
return $this->processResponse();
594+
}
595+
596+
// For raw requests, we only check for non-2xx status codes.
597+
if (!in_array($this->response->getCode(), ['200', '201', '202', '204'])) {
598+
// Try to parse the body as JSON to get a detailed error message
599+
$parsedError = \json_decode($this->response->getBody());
600+
if (JSON_ERROR_NONE === json_last_error()) {
601+
// It's a JSON error response, pass it to lookForErrors
602+
$this->lookForErrors($parsedError);
603+
} else {
604+
// Not a JSON error, create a generic exception
605+
throw new HttpClientException(
606+
'HTTP Error ' . $this->response->getCode(),
607+
$this->response->getCode(),
608+
$this->request,
609+
$this->response
610+
);
611+
}
612+
}
613+
614+
return $this->response;
565615
}
566616

567617
/**

tests/Integration/ClientIntegrationTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,27 @@ public function testCanDeleteAddress(): void
155155
$getResponse = $this->client->get('ADR/Adresse/' . $addressId);
156156
$this->assertTrue($getResponse->Geloescht);
157157
}
158+
159+
public function testCanGetList(): void
160+
{
161+
try {
162+
$response = $this->client->getList(1029); // Using a known list ID from Go tests
163+
164+
$this->assertEquals(200, $response->getCode());
165+
$this->assertNotEmpty($response->getBody());
166+
$headers = $response->getHeaders();
167+
$this->assertArrayHasKey('Content-Type', $headers);
168+
$this->assertEquals('application/pdf', $headers['Content-Type']);
169+
170+
} catch (HttpClientException $e) {
171+
// The list might not exist in all test environments. If so, skip the test.
172+
// A 404 on the final GET will be caught here.
173+
if ($e->getCode() === 404) {
174+
$this->markTestSkipped('List with ID 1029 not found or failed to generate. Skipping getList test.');
175+
} else {
176+
// Re-throw other exceptions
177+
throw $e;
178+
}
179+
}
180+
}
158181
}

0 commit comments

Comments
 (0)