feat(admin): normalize Persian/Arabic digits in every numeric field
Users typing on a Persian keyboard produced two distinct failures. Fields with type="number" silently returned an empty string — the browser rejects Persian digits, so the value was lost and saved as empty or zero. Text fields passed the Persian characters straight through to the database, where a mobile stored as ۰۹۱۲… never matches 09… again. The secretary form hit the second case with no validation at all. Frontend: - Adds digitsOnly() and the national-code schemas to lib/utils, plus lib/forms with numericField()/latinDigitsField() wrappers for React Hook Form fields. - Converts every type="number" input to type="text" inputMode="numeric" with digit normalization; none remain. Fields that legitimately carry non-digits (sheba, landline) only get the digits translated, keeping IR and separators. - Points the patient national-code and mobile schemas at the shared normalizing schemas, which accept Persian input instead of rejecting it. - Drops two duplicate local digit converters in favour of the shared helper. Backend: - Adds NumericFieldNormalizerSubscriber, translating digits in whitelisted numeric keys of JSON request bodies under /api/v1/ before controllers run, so nobat724_front and clinic-pro-tauri are covered too. Translation only — no characters are stripped, non-string values and other keys are untouched. Three component tests asserted on role="spinbutton" and numeric input values; both are properties of type="number", so they were updated to match the new text inputs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Shared\EventSubscriber;
|
||||
|
||||
use App\Shared\Util\PersianText;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
|
||||
/**
|
||||
* ارقام فارسی/عربی را در بدنهٔ JSON درخواستهای API به لاتین ترجمه میکند.
|
||||
*
|
||||
* پنل ادمین ورودیها را در مبدأ نرمال میکند، ولی `nobat724_front` و
|
||||
* `clinic-pro-tauri` هم همین API را صدا میزنند؛ این لایه تضمین میکند هیچ
|
||||
* کلاینتی نتواند رقم فارسی وارد دیتابیس کند.
|
||||
*
|
||||
* فقط ترجمهٔ رقم انجام میشود — کاراکتر غیرعددی حذف نمیشود چون شبا حرف `IR`
|
||||
* دارد و تلفن ثابت خط تیره.
|
||||
*/
|
||||
class NumericFieldNormalizerSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
/** کلیدهایی که مقدارشان عددی است و باید نرمال شوند. */
|
||||
private const NUMERIC_KEYS = [
|
||||
'mobile', 'mobile_number', 'telephone', 'phone', 'notification_mobile',
|
||||
'national_code', 'postal_code',
|
||||
'card_number', 'account_number', 'sheba', 'shaba', 'iban',
|
||||
'price_rials', 'amount_rials', 'amount', 'free_visit_price_rials',
|
||||
'insurance_price_rials', 'patient_share_rials', 'visit_price_rials',
|
||||
'duration_minutes', 'duration', 'commission_percent', 'coverage',
|
||||
'coverage_percent', 'franchise', 'ceiling', 'tax_percent',
|
||||
'base_insurance_discount_percent', 'supplementary_discount_percent',
|
||||
];
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [KernelEvents::REQUEST => ['onKernelRequest', 8]];
|
||||
}
|
||||
|
||||
public function onKernelRequest(RequestEvent $event): void
|
||||
{
|
||||
$request = $event->getRequest();
|
||||
|
||||
if (!str_starts_with($request->getPathInfo(), '/api/v1/')) {
|
||||
return;
|
||||
}
|
||||
if (!in_array($request->getMethod(), ['POST', 'PUT', 'PATCH'], true)) {
|
||||
return;
|
||||
}
|
||||
if (!str_contains((string) $request->headers->get('Content-Type'), 'json')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$content = $request->getContent();
|
||||
if ($content === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$data = json_decode($content, true);
|
||||
if (!is_array($data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$normalized = $this->normalizeTree($data);
|
||||
if ($normalized === $data) {
|
||||
return;
|
||||
}
|
||||
|
||||
$request->initialize(
|
||||
$request->query->all(),
|
||||
$request->request->all(),
|
||||
$request->attributes->all(),
|
||||
$request->cookies->all(),
|
||||
$request->files->all(),
|
||||
$request->server->all(),
|
||||
json_encode($normalized, JSON_UNESCAPED_UNICODE),
|
||||
);
|
||||
}
|
||||
|
||||
private function normalizeTree(array $data): array
|
||||
{
|
||||
foreach ($data as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
$data[$key] = $this->normalizeTree($value);
|
||||
continue;
|
||||
}
|
||||
if (is_string($value) && in_array((string) $key, self::NUMERIC_KEYS, true)) {
|
||||
$data[$key] = PersianText::digits($value);
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,20 @@ final class PersianText
|
||||
return trim(preg_replace('/\s+/u', ' ', $text) ?? $text);
|
||||
}
|
||||
|
||||
/**
|
||||
* فقط ارقام فارسی/عربی را به لاتین ترجمه میکند و بقیهٔ کاراکترها را دست نمیزند.
|
||||
*
|
||||
* برخلاف normalize() فاصلهها را جمع نمیکند و trim نمیکند — برای فیلدهای عددی
|
||||
* لازم است، چون شبا حرف دارد و تلفن ثابت خط تیره.
|
||||
*/
|
||||
public static function digits(string $text): string
|
||||
{
|
||||
return strtr($text, array_combine(
|
||||
['۰','۱','۲','۳','۴','۵','۶','۷','۸','۹','٠','١','٢','٣','٤','٥','٦','٧','٨','٩'],
|
||||
['0','1','2','3','4','5','6','7','8','9','0','1','2','3','4','5','6','7','8','9'],
|
||||
));
|
||||
}
|
||||
|
||||
/** مقایسهٔ دو نام فارسی پس از نرمالسازی. */
|
||||
public static function sameName(string $a, string $b): bool
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user