fix: normalize order response to valid UTF-8 before JSON encoding (#83)

* fix: normalize cart json utf8 and add tests

Ensure cart and checkout responses are cleaned to valid UTF-8 before JSON encoding and cover Utf8Cleaner with unit tests.

Made-with: Cursor

* fix: normalize order response to valid UTF-8 before JSON encoding

- Add Utf8Cleaner::clean() for successful order creation response (data)
- Add Utf8Cleaner::clean() for validation error response (ErrorBag)
- Prevents Malformed UTF-8 characters error when creating order with
  non-UTF-8 data from OpenCart/DB (same approach as cart endpoints)

Made-with: Cursor
This commit is contained in:
2026-03-14 15:36:09 +03:00
committed by GitHub
parent 3e6fed813d
commit f894d33c46
4 changed files with 145 additions and 4 deletions

View File

@@ -3,6 +3,7 @@
namespace App\Handlers; namespace App\Handlers;
use App\Services\CartService; use App\Services\CartService;
use App\Support\Utf8Cleaner;
use Cart\Cart; use Cart\Cart;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Openguru\OpenCartFramework\Http\Request; use Openguru\OpenCartFramework\Http\Request;
@@ -23,7 +24,7 @@ class CartHandler
$items = $this->cartService->getCart(); $items = $this->cartService->getCart();
return new JsonResponse([ return new JsonResponse([
'data' => $items, 'data' => Utf8Cleaner::clean($items),
]); ]);
} }
@@ -48,7 +49,7 @@ class CartHandler
} }
return new JsonResponse([ return new JsonResponse([
'data' => $items, 'data' => Utf8Cleaner::clean($items),
]); ]);
} }
} }

View File

@@ -4,6 +4,7 @@ namespace App\Handlers;
use App\Exceptions\OrderValidationFailedException; use App\Exceptions\OrderValidationFailedException;
use App\Services\OrderCreateService; use App\Services\OrderCreateService;
use App\Support\Utf8Cleaner;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Openguru\OpenCartFramework\Http\Request; use Openguru\OpenCartFramework\Http\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
@@ -26,11 +27,11 @@ class OrderHandler
]); ]);
return new JsonResponse([ return new JsonResponse([
'data' => $order, 'data' => Utf8Cleaner::clean($order),
], Response::HTTP_CREATED); ], Response::HTTP_CREATED);
} catch (OrderValidationFailedException $exception) { } catch (OrderValidationFailedException $exception) {
return new JsonResponse([ return new JsonResponse([
'data' => $exception->getErrorBag()->firstOfAll(), 'data' => Utf8Cleaner::clean($exception->getErrorBag()->firstOfAll()),
], Response::HTTP_UNPROCESSABLE_ENTITY); ], Response::HTTP_UNPROCESSABLE_ENTITY);
} }
} }

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Support;
class Utf8Cleaner
{
/**
* Рекурсивно проходит по массиву/объекту и приводит все строковые значения к валидному UTF-8.
*/
public static function clean($value)
{
if (is_array($value)) {
foreach ($value as $key => $item) {
$value[$key] = self::clean($item);
}
return $value;
}
if (is_object($value)) {
foreach (get_object_vars($value) as $property => $item) {
$value->{$property} = self::clean($item);
}
return $value;
}
if (is_string($value)) {
// Попробовать нормализовать строку в UTF-8, подменяя невалидные последовательности.
$converted = @mb_convert_encoding($value, 'UTF-8', 'UTF-8');
if ($converted === false) {
// Фолбэк: заменить невалидные байты стандартным алгоритмом json_encode.
$encoded = json_encode($value, JSON_INVALID_UTF8_SUBSTITUTE);
if ($encoded === false) {
return '';
}
// Убираем кавычки, которые добавляет json_encode.
return json_decode($encoded, true) ?? '';
}
return $converted;
}
return $value;
}
}

View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Support;
use App\Support\Utf8Cleaner;
use PHPUnit\Framework\TestCase;
class Utf8CleanerTest extends TestCase
{
/** @test */
public function returns_non_string_values_as_is(): void
{
$this->assertSame(123, Utf8Cleaner::clean(123));
$this->assertSame(12.3, Utf8Cleaner::clean(12.3));
$this->assertSame(true, Utf8Cleaner::clean(true));
$this->assertSame(false, Utf8Cleaner::clean(false));
$this->assertNull(Utf8Cleaner::clean(null));
}
/** @test */
public function leaves_valid_utf8_strings_unchanged(): void
{
$value = 'Привет, мир! 😀';
$this->assertSame($value, Utf8Cleaner::clean($value));
}
/** @test */
public function cleans_strings_with_invalid_utf8_bytes(): void
{
// Строка с заведомо некорректной UTF-8 последовательностью.
$invalid = "\xF0\x28\x8C\x28";
$cleaned = Utf8Cleaner::clean($invalid);
$this->assertIsString($cleaned);
$this->assertNotSame($invalid, $cleaned);
$json = json_encode($cleaned);
$this->assertNotFalse($json, 'Resulting string must be valid UTF-8 for json_encode');
}
/** @test */
public function cleans_nested_arrays_recursively(): void
{
$invalid = "\xF0\x28\x8C\x28";
$data = [
'foo' => 'bar',
'nested' => [
'value' => $invalid,
],
];
$cleaned = Utf8Cleaner::clean($data);
$this->assertIsArray($cleaned);
$this->assertSame('bar', $cleaned['foo']);
$this->assertIsString($cleaned['nested']['value']);
$this->assertNotSame($invalid, $cleaned['nested']['value']);
$json = json_encode($cleaned);
$this->assertNotFalse($json);
}
/** @test */
public function cleans_objects_recursively(): void
{
$invalid = "\xF0\x28\x8C\x28";
$object = new class {
public string $name = 'test';
public array $meta = [];
};
$object->meta['invalid'] = $invalid;
$cleaned = Utf8Cleaner::clean($object);
$this->assertSame('test', $cleaned->name);
$this->assertIsString($cleaned->meta['invalid']);
$this->assertNotSame($invalid, $cleaned->meta['invalid']);
$json = json_encode($cleaned);
$this->assertNotFalse($json);
}
}