From cae41e72b84ae3f7413f7021c958c3a9ad004a8d Mon Sep 17 00:00:00 2001 From: adonis Date: Tue, 1 Sep 2026 21:38:09 +0330 Subject: [PATCH] fix(search): match Arabic ye/kaf with Persian letters in admin search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Customer names stored with ي/ك were missed when typing ی/ک; fold both sides of LIKE. Co-authored-by: Cursor --- app/DB/Te/QuerySelector.php | 7 ++++- app/DB/Te/Search.php | 30 ++++++++++--------- .../Admin/AppNotificationController.php | 6 +++- .../CustomerSpecialPricePackageController.php | 6 +++- app/Support/PersianText.php | 25 ++++++++++++++++ 5 files changed, 57 insertions(+), 17 deletions(-) create mode 100644 app/Support/PersianText.php diff --git a/app/DB/Te/QuerySelector.php b/app/DB/Te/QuerySelector.php index de430b6..40345f8 100644 --- a/app/DB/Te/QuerySelector.php +++ b/app/DB/Te/QuerySelector.php @@ -3,6 +3,7 @@ namespace App\DB\Te; +use App\Support\PersianText; use Illuminate\Http\Request; use Illuminate\Support\Arr; use Symfony\Component\HttpFoundation\Exception\BadRequestException; @@ -37,7 +38,11 @@ class QuerySelector } $cols = explode(',', $conf->cols); if (isset($input['query'])) { - $query = $conf->model::where($cols[0], 'like', '%' . $input['query'] . '%'); + $needle = PersianText::likePattern(strtolower((string) $input['query'])); + $query = $conf->model::whereRaw( + PersianText::sqlFoldExpr('lower(`'.$cols[0].'`)').' like ?', + [$needle] + ); } diff --git a/app/DB/Te/Search.php b/app/DB/Te/Search.php index 8de3d9a..1a470ca 100644 --- a/app/DB/Te/Search.php +++ b/app/DB/Te/Search.php @@ -1,5 +1,8 @@ where(function ($q) use ($cols, $search) { + $like = PersianText::likePattern(strtolower((string) $search)); + $query->where(function ($q) use ($cols, $like) { $first = true; foreach ($cols as $col) { if (self::hasJoin($col)) { @@ -25,11 +29,9 @@ class Search $whereHas = 'orWhereHas'; } $q->$whereHas - ($exp[0], function ($q) use ($exp, $search) { - return $q->whereHas($exp[1], function ($q2) use ($exp, $search) { - -// return $q2->where($exp[2], 'LIKE', '%' . $search . '%'); - return $q2->whereRaw('lower(`'.$exp[2].'`) like ?', ['%' . strtolower($search) . '%']); + ($exp[0], function ($q) use ($exp, $like) { + return $q->whereHas($exp[1], function ($q2) use ($exp, $like) { + return $q2->whereRaw(self::foldedLikeSql($exp[2]), [$like]); }); }); } else { @@ -39,10 +41,8 @@ class Search $whereHas1 = 'orWhereHas'; } $q->$whereHas1 -// $q->{($first) ? 'whereHas' : 'orWhereHas'} - ($exp[0], function ($q) use ($exp, $search) { - return $q->whereRaw('lower(`'.$exp[1].'`) like ?', ['%' . strtolower($search) . '%']); -// return $q->where($exp[1], 'LIKE', '%' . $search . '%'); + ($exp[0], function ($q) use ($exp, $like) { + return $q->whereRaw(self::foldedLikeSql($exp[1]), [$like]); }); } } else { @@ -51,10 +51,7 @@ class Search }else{ $where = 'orWhereRaw'; } -// return $q->whereRaw('lower(`'.$col.'`) like ?', ['%' . strtolower($search) . '%']); - $q->$where('lower(`'.$col.'`) like ?', ['%' . strtolower($search) . '%']); -// $q->{($first) ? 'where' : 'orWhere'}($col, 'LIKE', '%' . $search . '%'); -// $q->{($first) ? 'whereRaw ' : 'orWhereRaw '}($col, 'LIKE', '%' . $search . '%'); + $q->$where(self::foldedLikeSql($col), [$like]); } if ($first) { $first = false; @@ -63,6 +60,11 @@ class Search }); } + private static function foldedLikeSql(string $column): string + { + return PersianText::sqlFoldExpr('lower(`'.$column.'`)').' like ?'; + } + public static function hasJoin($col) { return sizeof(explode('.', $col)) > 1; diff --git a/app/Http/Controllers/Admin/AppNotificationController.php b/app/Http/Controllers/Admin/AppNotificationController.php index e199fed..fc3cf47 100644 --- a/app/Http/Controllers/Admin/AppNotificationController.php +++ b/app/Http/Controllers/Admin/AppNotificationController.php @@ -10,6 +10,7 @@ use App\Models\AppNotificationRecipient; use App\Models\Customer; use App\Services\AppNotificationAudienceService; use App\Services\PriceStreamService; +use App\Support\PersianText; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; @@ -67,7 +68,10 @@ class AppNotificationController extends Controller ->where('is_archive', false) ->when($q !== '', function ($query) use ($q) { $query->where(function ($inner) use ($q) { - $inner->where('name', 'like', '%' . $q . '%') + $inner->whereRaw( + PersianText::sqlFoldExpr('lower(`name`)').' like ?', + [PersianText::likePattern(strtolower($q))] + ) ->orWhere('mobile', 'like', '%' . $q . '%') ->orWhere('id', $q); }); diff --git a/app/Http/Controllers/Admin/CustomerSpecialPricePackageController.php b/app/Http/Controllers/Admin/CustomerSpecialPricePackageController.php index 57e1deb..0e21e17 100644 --- a/app/Http/Controllers/Admin/CustomerSpecialPricePackageController.php +++ b/app/Http/Controllers/Admin/CustomerSpecialPricePackageController.php @@ -9,6 +9,7 @@ use App\Models\CustomerSpecialPricePackage; use App\Models\Product; use App\Services\ConfigService; use App\Services\CustomerSpecialPricingService; +use App\Support\PersianText; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; @@ -163,7 +164,10 @@ class CustomerSpecialPricePackageController extends Controller }) ->when($q !== '', function ($query) use ($q) { $query->where(function ($inner) use ($q) { - $inner->where('name', 'like', '%' . $q . '%') + $inner->whereRaw( + PersianText::sqlFoldExpr('lower(`name`)').' like ?', + [PersianText::likePattern(strtolower($q))] + ) ->orWhere('mobile', 'like', '%' . $q . '%') ->orWhere('id', $q); }); diff --git a/app/Support/PersianText.php b/app/Support/PersianText.php new file mode 100644 index 0000000..5efe1e4 --- /dev/null +++ b/app/Support/PersianText.php @@ -0,0 +1,25 @@ +