Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
45 changes: 45 additions & 0 deletions src/RestAPIWrapperProffix/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Pitwch\RestAPIWrapperProffix;

use Pitwch\RestAPIWrapperProffix\HttpClient\HttpClient;
use Pitwch\RestAPIWrapperProffix\HttpClient\HttpClientException;

/**
* Class Client
Expand Down Expand Up @@ -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);
}
}

124 changes: 87 additions & 37 deletions src/RestAPIWrapperProffix/HttpClient/HttpClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}

/**
Expand Down Expand Up @@ -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);
Expand All @@ -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;
}

/**
Expand Down
23 changes: 23 additions & 0 deletions tests/Integration/ClientIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
}
Loading