Files
MtoRagSystem/src/Shopware/StoreApiClient.php
2026-04-25 12:19:20 +02:00

152 lines
4.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Shopware;
use RuntimeException;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
final readonly class StoreApiClient
{
public function __construct(
private HttpClientInterface $httpClient,
private string $baseUrl,
private string $salesChannelAccessKey,
private int $timeoutSeconds = 5,
) {
}
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
* @throws StoreApiException
*/
public function searchProducts(array $criteria): array
{
$url = rtrim($this->baseUrl, '/') . '/store-api/search';
$sanitizedCriteria = $this->sanitizeValue($criteria);
$body = json_encode(
$sanitizedCriteria,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE
);
if (!is_string($body)) {
throw new RuntimeException('Failed to encode Store API criteria.');
}
$response = $this->httpClient->request('POST', $url, [
'headers' => [
'Content-Type' => 'application/json; charset=utf-8',
'Accept' => 'application/json',
'sw-access-key' => $this->salesChannelAccessKey,
],
'body' => $body,
'timeout' => $this->timeoutSeconds,
]);
$statusCode = $response->getStatusCode();
$content = $response->getContent(false);
$content = $this->sanitizeString($content);
if ($statusCode < 200 || $statusCode >= 300) {
throw $this->buildHttpFailure($statusCode, $content);
}
$data = json_decode($content, true);
if (!is_array($data)) {
throw new StoreApiException(
'Shopware Store API returned invalid JSON.',
$statusCode,
true,
$this->containsUtf8FailureSignal($content),
true
);
}
return $data;
}
private function buildHttpFailure(int $statusCode, string $content): StoreApiException
{
$preview = mb_substr(trim($content), 0, 1000);
$utf8Failure = $this->containsUtf8FailureSignal($preview);
$serverFailure = $statusCode >= 500;
return new StoreApiException(
sprintf(
'Shopware Store API request failed with status %d. Response: %s',
$statusCode,
$preview
),
$statusCode,
$serverFailure,
$utf8Failure,
$serverFailure || $utf8Failure
);
}
private function containsUtf8FailureSignal(string $content): bool
{
$normalized = mb_strtolower($content, 'UTF-8');
return str_contains($normalized, 'malformed utf-8')
|| str_contains($normalized, 'malformed utf8')
|| str_contains($normalized, 'invalid utf-8')
|| str_contains($normalized, 'invalid utf8')
|| str_contains($normalized, 'possibly incorrectly encoded');
}
private function sanitizeValue(mixed $value): mixed
{
if (is_array($value)) {
$out = [];
foreach ($value as $key => $item) {
$out[$key] = $this->sanitizeValue($item);
}
return $out;
}
if (!is_string($value)) {
return $value;
}
return $this->sanitizeString($value);
}
private function sanitizeString(string $value): string
{
if (preg_match('//u', $value) === 1) {
return $value;
}
if (function_exists('mb_convert_encoding')) {
$value = mb_convert_encoding($value, 'UTF-8', 'UTF-8');
}
if (preg_match('//u', $value) === 1) {
return $value;
}
if (function_exists('iconv')) {
$converted = @iconv('UTF-8', 'UTF-8//IGNORE', $value);
if (is_string($converted) && $converted !== '') {
return $converted;
}
}
return '';
}
}