Skip to content

Commit 9198003

Browse files
committed
Implement accent-insensitive search feature for Portuguese characters
- Add 'ignore_accents' config option in search settings - Implement Helper::normalizeAccents() for Portuguese accent normalization - Add Config::isIgnoreAccents() method to check configuration - Update QueryDataTable to handle accent normalization in database queries - Update CollectionDataTable to handle accent normalization in collection filtering - Add comprehensive unit tests for accent normalization - Support for Portuguese Brazilian accents: ã/á/à/â/é/ê/í/ó/ô/õ/ú/ç This allows users to search for 'simoes' and find 'Simões' when the feature is enabled. Fixes #3249
1 parent 79c601a commit 9198003

File tree

8 files changed

+197
-1
lines changed

8 files changed

+197
-1
lines changed

src/CollectionDataTable.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Illuminate\Support\Arr;
1111
use Illuminate\Support\Collection;
1212
use Illuminate\Support\Str;
13+
use Yajra\DataTables\Utilities\Helper;
1314

1415
class CollectionDataTable extends DataTableAbstract
1516
{
@@ -106,6 +107,11 @@ function ($row) use ($column, $keyword, $regex) {
106107
/** @var string $value */
107108
$value = Arr::get($data, $column);
108109

110+
if ($this->config->isIgnoreAccents()) {
111+
$value = Helper::normalizeAccents($value);
112+
$keyword = Helper::normalizeAccents($keyword);
113+
}
114+
109115
if ($this->config->isCaseInsensitive()) {
110116
if ($regex) {
111117
return preg_match('/'.$keyword.'/i', $value) == 1;
@@ -215,6 +221,10 @@ public function setOffset(int $offset): self
215221
*/
216222
protected function globalSearch(string $keyword): void
217223
{
224+
if ($this->config->isIgnoreAccents()) {
225+
$keyword = Helper::normalizeAccents($keyword);
226+
}
227+
218228
$keyword = $this->config->isCaseInsensitive() ? Str::lower($keyword) : $keyword;
219229

220230
$this->collection = $this->collection->filter(function ($row) use ($keyword) {
@@ -225,6 +235,9 @@ protected function globalSearch(string $keyword): void
225235
if (! is_string($value)) {
226236
continue;
227237
} else {
238+
if ($this->config->isIgnoreAccents()) {
239+
$value = Helper::normalizeAccents($value);
240+
}
228241
$value = $this->config->isCaseInsensitive() ? Str::lower($value) : $value;
229242
}
230243

src/QueryDataTable.php

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -568,7 +568,10 @@ protected function compileQuerySearch($query, string $column, string $keyword, s
568568
$column = $this->castColumn($column);
569569
$sql = $column.' LIKE ?';
570570

571-
if ($this->config->isCaseInsensitive()) {
571+
if ($this->config->isIgnoreAccents()) {
572+
// For accent-insensitive search, we normalize both the column and the keyword
573+
$sql = $this->getNormalizeAccentsFunction($column).' LIKE ?';
574+
} elseif ($this->config->isCaseInsensitive()) {
572575
$sql = 'LOWER('.$column.') LIKE ?';
573576
}
574577

@@ -680,6 +683,10 @@ protected function getSelectedColumns($query): array
680683
*/
681684
protected function prepareKeyword(string $keyword): string
682685
{
686+
if ($this->config->isIgnoreAccents()) {
687+
$keyword = Helper::normalizeAccents($keyword);
688+
}
689+
683690
if ($this->config->isCaseInsensitive()) {
684691
$keyword = Str::lower($keyword);
685692
}
@@ -699,6 +706,21 @@ protected function prepareKeyword(string $keyword): string
699706
return $keyword;
700707
}
701708

709+
/**
710+
* Get the database function to normalize accents for the given column.
711+
*/
712+
protected function getNormalizeAccentsFunction(string $column): string
713+
{
714+
$driver = $this->getConnection()->getDriverName();
715+
716+
return match ($driver) {
717+
'mysql' => "REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(LOWER($column), 'ã', 'a'), 'á', 'a'), 'à', 'a'), 'â', 'a'), 'é', 'e'), 'ê', 'e'), 'í', 'i'), 'ó', 'o'), 'ô', 'o'), 'õ', 'o'), 'ú', 'u'), 'ç', 'c'), 'ã', 'a'), 'á', 'a'), 'à', 'a'), 'â', 'a')",
718+
'pgsql' => "LOWER(translate($column, 'ÃãÁáÀàÂâÉéÊêÍíÓóÔôÕõÚúÇç', 'aaaaaaaeeeiioooooucc'))",
719+
'sqlite' => "LOWER($column)", // SQLite doesn't have built-in accent normalization, so we'll rely on keyword normalization only
720+
default => "LOWER($column)" // Fallback for other databases
721+
};
722+
}
723+
702724
/**
703725
* Add custom filter handler for the give column.
704726
*

src/Utilities/Config.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ public function isStartsWithSearch(): bool
8181
return (bool) $this->repository->get('datatables.search.starts_with', false);
8282
}
8383

84+
/**
85+
* Check if dataTable config ignores accents when searching.
86+
*/
87+
public function isIgnoreAccents(): bool
88+
{
89+
return (bool) $this->repository->get('datatables.search.ignore_accents', false);
90+
}
91+
8492
public function jsonOptions(): int
8593
{
8694
/** @var int $options */

src/Utilities/Helper.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,23 @@
1212
use ReflectionMethod;
1313

1414
class Helper
15+
16+
/**
17+
* Normalize accented characters to their base letter for accent-insensitive search.
18+
* Only replaces Portuguese Brazilian accents as specified.
19+
*/
20+
public static function normalizeAccents(string $value): string
21+
{
22+
$map = [
23+
'Ã' => 'a', 'ã' => 'a', 'Á' => 'a', 'á' => 'a', 'À' => 'a', 'à' => 'a', 'Â' => 'a', 'â' => 'a',
24+
'É' => 'e', 'é' => 'e', 'Ê' => 'e', 'ê' => 'e',
25+
'Í' => 'i', 'í' => 'i',
26+
'Ó' => 'o', 'ó' => 'o', 'Ô' => 'o', 'ô' => 'o', 'Õ' => 'o', 'õ' => 'o',
27+
'Ú' => 'u', 'ú' => 'u',
28+
'Ç' => 'c', 'ç' => 'c',
29+
];
30+
return strtr($value, $map);
31+
}
1532
{
1633
/**
1734
* Places item of extra columns into results by care of their order.

src/config/datatables.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@
3333
* SQL: column LIKE "keyword%"
3434
*/
3535
'starts_with' => false,
36+
37+
/*
38+
* Ignore accents when filtering/searching (accent-insensitive search).
39+
* If true, accented characters will be normalized to their base letter.
40+
* Example: 'Simões' will match 'simoes'.
41+
*/
42+
'ignore_accents' => false,
3643
],
3744

3845
/*

test_accent_normalization.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
require_once __DIR__ . '/vendor/autoload.php';
4+
5+
use Yajra\DataTables\Utilities\Helper;
6+
7+
// Test the normalizeAccents function
8+
echo "Testing Helper::normalizeAccents() function:\n";
9+
echo "==================================================\n";
10+
11+
$testCases = [
12+
'Tatiane Simões' => 'tatiane simoes',
13+
'João' => 'joao',
14+
'São Paulo' => 'sao paulo',
15+
'José' => 'jose',
16+
'Ação' => 'acao',
17+
'Coração' => 'coracao',
18+
'Não' => 'nao',
19+
'Canção' => 'cancao',
20+
];
21+
22+
foreach ($testCases as $input => $expected) {
23+
$result = strtolower(Helper::normalizeAccents($input));
24+
$status = $result === $expected ? '✅ PASS' : '❌ FAIL';
25+
26+
echo "Input: '$input'\n";
27+
echo "Expected: '$expected'\n";
28+
echo "Result: '$result'\n";
29+
echo "Status: $status\n";
30+
echo "---\n";
31+
}
32+
33+
echo "\nAll tests completed!\n";

tests/Unit/ConfigTest.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
namespace Yajra\DataTables\Tests\Unit;
4+
5+
use Yajra\DataTables\Tests\TestCase;
6+
use Yajra\DataTables\Utilities\Config;
7+
8+
class ConfigTest extends TestCase
9+
{
10+
/** @var Config */
11+
private $config;
12+
13+
public function setUp(): void
14+
{
15+
parent::setUp();
16+
$this->config = app('datatables.config');
17+
}
18+
19+
public function test_is_ignore_accents_default()
20+
{
21+
config(['datatables.search.ignore_accents' => false]);
22+
$this->assertFalse($this->config->isIgnoreAccents());
23+
}
24+
25+
public function test_is_ignore_accents_enabled()
26+
{
27+
config(['datatables.search.ignore_accents' => true]);
28+
$this->assertTrue($this->config->isIgnoreAccents());
29+
}
30+
31+
public function test_is_ignore_accents_with_null_config()
32+
{
33+
config(['datatables.search.ignore_accents' => null]);
34+
$this->assertFalse($this->config->isIgnoreAccents());
35+
}
36+
37+
public function test_is_ignore_accents_with_string_true()
38+
{
39+
config(['datatables.search.ignore_accents' => 'true']);
40+
$this->assertTrue($this->config->isIgnoreAccents());
41+
}
42+
43+
public function test_is_ignore_accents_with_string_false()
44+
{
45+
config(['datatables.search.ignore_accents' => 'false']);
46+
$this->assertTrue($this->config->isIgnoreAccents()); // non-empty string is truthy
47+
}
48+
49+
public function test_is_ignore_accents_with_zero()
50+
{
51+
config(['datatables.search.ignore_accents' => 0]);
52+
$this->assertFalse($this->config->isIgnoreAccents());
53+
}
54+
55+
public function test_is_ignore_accents_with_one()
56+
{
57+
config(['datatables.search.ignore_accents' => 1]);
58+
$this->assertTrue($this->config->isIgnoreAccents());
59+
}
60+
}

tests/Unit/HelperTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,4 +281,40 @@ public function test_wildcard_string()
281281

282282
$this->assertEquals('.*k.*e.*y.*w.*o.*r.*d.*', $keyword);
283283
}
284+
285+
public function test_normalize_accents()
286+
{
287+
// Test Portuguese Brazilian accents
288+
$testCases = [
289+
'Tatiane Simões' => 'Tatiane Simoes',
290+
'João' => 'Joao',
291+
'São Paulo' => 'Sao Paulo',
292+
'José' => 'Jose',
293+
'Ação' => 'Acao',
294+
'Coração' => 'Coracao',
295+
'Não' => 'Nao',
296+
'Canção' => 'Cancao',
297+
// Test all accent mappings individually
298+
'ãáàâ' => 'aaaa',
299+
'ÃÁÀÂ' => 'AAAA',
300+
'éê' => 'ee',
301+
'ÉÊ' => 'EE',
302+
'í' => 'i',
303+
'Í' => 'I',
304+
'óôõ' => 'ooo',
305+
'ÓÔÕ' => 'OOO',
306+
'ú' => 'u',
307+
'Ú' => 'U',
308+
'ç' => 'c',
309+
'Ç' => 'C',
310+
// Test mixed content
311+
'Não há ação' => 'Nao ha acao',
312+
'Coração de São João' => 'Coracao de Sao Joao',
313+
];
314+
315+
foreach ($testCases as $input => $expected) {
316+
$result = Helper::normalizeAccents($input);
317+
$this->assertEquals($expected, $result, "Failed to normalize '$input'");
318+
}
319+
}
284320
}

0 commit comments

Comments
 (0)