Skip to content

Commit a029b94

Browse files
committed
Sorting and searching trait
1 parent 4d768e6 commit a029b94

6 files changed

Lines changed: 203 additions & 13 deletions

File tree

src/Shared/Core/App.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Ivy\Shared\Infrastructure\Manager\LanguageManager;
1717
use Ivy\Shared\Infrastructure\Manager\RouterManager;
1818
use Ivy\Shared\Infrastructure\Manager\SecurityManager;
19+
use Ivy\Shared\Infrastructure\Service\SortService;
1920
use Ivy\Template\Infrastructure\Manager\TemplateManager;
2021
use Ivy\Template\Presentation\View\Engine\BladeEngine;
2122
use Ivy\User\Application\Service\AuthService;
@@ -116,6 +117,8 @@ public function run(): void
116117
$this->container->instance(Request::class, $request);
117118
Container::setInstance($this->container);
118119

120+
$this->container->singleton(SortService::class);
121+
119122
$pipeline = new MiddlewarePipeline;
120123
$pipeline->add(new RequestNormalizer());
121124
$pipeline->add(new CsrfVerifier());
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace Ivy\Shared\Infrastructure\Service;
4+
5+
use Illuminate\Database\Eloquent\Builder;
6+
7+
class RelationPathService
8+
{
9+
public function resolve(object $model, string $path, Builder $query): array
10+
{
11+
$segments = explode('.', $path);
12+
$field = array_pop($segments);
13+
14+
$parent = $model;
15+
$parentTable = $model->getTable();
16+
17+
foreach ($segments as $relationName) {
18+
19+
$relation = $parent->{$relationName}();
20+
$related = $relation->getRelated();
21+
$relatedTable = $related->getTable();
22+
23+
$query->leftJoin(
24+
$relatedTable,
25+
$relation->getQualifiedParentKeyName(),
26+
'=',
27+
$relation->getQualifiedForeignKeyName()
28+
);
29+
30+
$parent = $related;
31+
$parentTable = $relatedTable;
32+
}
33+
34+
return [
35+
'table' => $parentTable,
36+
'field' => $field,
37+
];
38+
}
39+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
namespace Ivy\Shared\Infrastructure\Service;
4+
5+
use Illuminate\Database\Eloquent\Builder;
6+
7+
class SearchService
8+
{
9+
public function apply(
10+
Builder $query,
11+
string $term,
12+
array $columns
13+
): Builder {
14+
15+
if ($term === '' || empty($columns)) {
16+
return $query;
17+
}
18+
19+
$model = $query->getModel();
20+
$baseTable = $model->getTable();
21+
22+
$query->where(function (Builder $q) use ($baseTable, $term, $columns) {
23+
24+
$isFirst = true;
25+
26+
foreach ($columns as $column) {
27+
28+
// local column
29+
if (!str_contains($column, '.')) {
30+
31+
if ($isFirst) {
32+
$q->where(
33+
"$baseTable.$column",
34+
'LIKE',
35+
"%{$term}%"
36+
);
37+
} else {
38+
$q->orWhere(
39+
"$baseTable.$column",
40+
'LIKE',
41+
"%{$term}%"
42+
);
43+
}
44+
45+
$isFirst = false;
46+
continue;
47+
}
48+
49+
// relation column
50+
$this->applyRelationSearch($q, $column, $term, $isFirst);
51+
52+
$isFirst = false;
53+
}
54+
});
55+
56+
return $query;
57+
}
58+
59+
protected function applyRelationSearch(
60+
Builder $query,
61+
string $path,
62+
string $term,
63+
bool $useWhereInsteadOfOr = false
64+
): void {
65+
66+
$segments = explode('.', $path);
67+
$field = array_pop($segments);
68+
$relationPath = implode('.', $segments);
69+
70+
$method = $useWhereInsteadOfOr ? 'whereHas' : 'orWhereHas';
71+
72+
$query->{$method}($relationPath, function (Builder $q) use ($field, $term) {
73+
$q->where($field, 'LIKE', "%{$term}%");
74+
});
75+
}
76+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
namespace Ivy\Shared\Infrastructure\Service;
4+
5+
use Illuminate\Database\Eloquent\Builder;
6+
7+
class SortService
8+
{
9+
public function __construct(
10+
private RelationPathService $relationPathService
11+
) {}
12+
13+
public function apply(
14+
Builder $query,
15+
string $column,
16+
array $sortable,
17+
string $defaultColumn = 'id',
18+
string $direction = 'asc'
19+
): Builder {
20+
21+
if (!in_array($column, $sortable, true)) {
22+
$column = $defaultColumn;
23+
}
24+
25+
if (!in_array($direction, ['asc', 'desc'], true)) {
26+
$direction = 'asc';
27+
}
28+
29+
$query->reorder();
30+
31+
$model = $query->getModel();
32+
$baseTable = $model->getTable();
33+
34+
if (!str_contains($column, '.')) {
35+
return $query
36+
->orderBy("$baseTable.$column", $direction)
37+
->addSelect("$baseTable.*");
38+
}
39+
40+
$resolved = $this->relationPathService->resolve($model, $column, $query);
41+
42+
return $query
43+
->orderBy("{$resolved['table']}.{$resolved['field']}", $direction)
44+
->addSelect("$baseTable.*");
45+
}
46+
}

src/Shared/Traits/HasSearching.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace Ivy\Shared\Traits;
4+
5+
use Illuminate\Database\Eloquent\Builder;
6+
use Symfony\Component\HttpFoundation\Request;
7+
use Illuminate\Container\Container;
8+
use Ivy\Shared\Infrastructure\Service\SearchService;
9+
10+
trait HasSearching
11+
{
12+
public function scopeSearch(
13+
Builder $query,
14+
Request $request,
15+
): Builder {
16+
17+
$columns = static::$searchable ?? [];
18+
19+
$term = trim((string) (
20+
$request->query->get('search') ?? $request->request->get('search') ?? ''
21+
));
22+
23+
if ($term === '' || empty($columns)) {
24+
return $query;
25+
}
26+
27+
return Container::getInstance()->get(SearchService::class)->apply($query, $term, $columns);
28+
}
29+
}

src/Shared/Traits/HasSorting.php

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
namespace Ivy\Shared\Traits;
44

55
use Illuminate\Database\Eloquent\Builder;
6+
use Ivy\Shared\Infrastructure\Service\SortService;
67
use Symfony\Component\HttpFoundation\Request;
8+
use Illuminate\Container\Container;
79

810
trait HasSorting
911
{
@@ -13,29 +15,24 @@ public function scopeSort(
1315
string $defaultColumn = 'id',
1416
string $defaultDirection = 'asc'
1517
): Builder {
16-
$columns = static::$sortable ?? ['id'];
1718

18-
if (!in_array($defaultColumn, $columns, true) && $defaultColumn !== 'id') {
19-
throw new \InvalidArgumentException(
20-
"Invalid default sort column [$defaultColumn]"
21-
);
22-
}
19+
$columns = static::$sortable ?? ['id'];
2320

2421
$column = $request->query->get('sort', $defaultColumn);
2522
$direction = strtolower(
2623
$request->query->get('direction', $defaultDirection)
2724
);
2825

29-
if (!in_array($column, $columns, true)) {
30-
$column = $defaultColumn;
31-
}
32-
3326
if (!in_array($direction, ['asc', 'desc'], true)) {
3427
$direction = $defaultDirection;
3528
}
3629

37-
return $query
38-
->reorder()
39-
->orderBy($column, $direction);
30+
return Container::getInstance()->get(SortService::class)->apply(
31+
$query,
32+
$column,
33+
$columns,
34+
$defaultColumn,
35+
$direction
36+
);
4037
}
4138
}

0 commit comments

Comments
 (0)