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:
@@ -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),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user