ldc-boutique-cnc-sable/app/Services/DataTableService.php

278 lines
8.5 KiB
PHP
Raw Permalink Normal View History

2026-06-14 21:32:07 +02:00
<?php
declare(strict_types=1);
namespace App\Services;
use Illuminate\Contracts\Database\Query\Builder;
use Illuminate\Contracts\Session\Session;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Schema;
use Shared\Enums\BaseEnum;
use Shared\Pagination\Pagination;
2026-06-14 21:32:07 +02:00
use SortDirection;
class DataTableService
{
private const string ORDER_COLUMN = 'order';
private const string RESET_SORT_KEY = 'rst';
private const string CACHE_SORTED_KEY = 'sorted';
private const string CACHE_SORT_COL_KEY = 'sort_col';
private const string CACHE_SORT_WAY_KEY = 'sort_way';
public function __construct(
protected readonly Request $request,
protected readonly Session $session,
) {
}
/**
* Build Search query on specified fields
* splitting search into words or using whole sentence
*
* @template TModel of \Illuminate\Database\Eloquent\Model
* @template TBuilder of \Illuminate\Contracts\Database\Query\Builder
*
* @param class-string<TModel> $modelClass
* @param TBuilder $query
* @param (\Closure(TBuilder):(void|TBuilder))|null $searchQuery
*/
public function searchQuery(
string $modelClass,
Builder $query,
string $search,
?\Closure $searchQuery = null,
string ...$fields
): Builder {
if (empty($fields)) {
return $query;
}
$words = array_filter(explode(' ', $search));
$query->where(function ($q) use ($modelClass, $searchQuery, $words, $search, $fields): void {
// * Searching words
foreach ($words as $word) {
$this->searchOnFields($modelClass, $q, $word, ...$fields);
}
// * Searching whole sentence
$this->searchOnFields($modelClass, $q, $search, ...$fields);
if ($searchQuery) {
$searchQuery($q);
}
});
return $query;
}
/** @var array<string, array<int, string>> */
private static array $columnsCache = [];
public static function clearColumnsCache(): void
{
self::$columnsCache = [];
}
private function hasTableColumn(string $table, string $column): bool
{
if (! isset(self::$columnsCache[$table])) {
self::$columnsCache[$table] = Schema::getColumnListing($table);
}
return in_array($column, self::$columnsCache[$table], true);
}
2026-06-14 21:32:07 +02:00
/**
* Sort columns with a query
*
* @template TModel of \Illuminate\Database\Eloquent\Model
* @template TBuilder of \Illuminate\Contracts\Database\Query\Builder
*
* @param class-string<TModel> $modelClass
* @param TBuilder $query
* @param array<int, string> $ignore Columns to ignore for sorting.
* @param array<string, string> $columnMap Map virtual column names to database columns.
2026-06-14 21:32:07 +02:00
*/
public function sortQuery(
string $modelClass,
Builder $query,
array $ignore = [],
?string $defaultSortCol = null,
SortDirection $defaultSortWay = SortDirection::Ascending,
array $columnMap = [],
2026-06-14 21:32:07 +02:00
): Builder {
$table = (new $modelClass())->getTable();
$rName = $this->request->route()?->getName() ?? 'noroute';
if ($this->request->has(self::RESET_SORT_KEY)) {
$this->resetSortSession($rName);
}
$sort = $this->getSortRequest();
$sort_col = $sort['sort_col'];
$sort_way = $sort['sort_way'];
if ((! $sort_col || $sort_col === 'undefined') && $defaultSortCol !== null) {
$sort_col = $defaultSortCol;
$sort_way = $defaultSortWay;
}
2026-06-14 21:32:07 +02:00
$this->persistSortSession($rName, $sort_col, $sort_way);
$dbCol = $columnMap[$sort_col] ?? $sort_col;
if ($sort_col && $sort_col !== 'undefined' && $sort_way && ! in_array($sort_col, $ignore) && $this->hasTableColumn($table, $dbCol)) {
2026-06-14 21:32:07 +02:00
$direction = match ($sort_way) {
SortDirection::Descending => 'desc',
default => 'asc',
};
$query->orderBy("{$table}.{$dbCol}", $direction);
} elseif ($this->hasTableColumn($table, self::ORDER_COLUMN)) {
2026-06-14 21:32:07 +02:00
$query->orderBy(self::ORDER_COLUMN, 'asc');
$this->persistSortSession(
$rName,
self::ORDER_COLUMN,
SortDirection::Ascending
);
}
return $query;
}
/**
* @template TModel of \Illuminate\Database\Eloquent\Model
*
* @param class-string<TModel> $modelClass
*/
private function searchOnFields(
string $modelClass,
Builder $query,
string $search,
string ...$fields
): Builder {
$table = (new $modelClass())->getTable();
$searchTerm = "%{$search}%";
foreach ($fields as $field) {
$field = str_replace('?', '\?', $field);
$searchValue = $this->getSearchValue(
$modelClass,
$search,
$field
);
if (is_array($searchValue)) {
foreach ($searchValue as $enumItem) {
$query->orWhereLike(
"{$table}.{$field}",
"%{$enumItem->value}%"
);
}
continue;
}
$query->orWhereLike("{$table}.{$field}", $searchTerm);
}
return $query;
}
/**
* @template TModel of \Illuminate\Database\Eloquent\Model
*
* @param class-string<TModel> $modelClass
* @return string|array<int,\BackedEnum>
*/
private function getSearchValue(
string $modelClass,
string $search,
string $field
) {
$cast = (new $modelClass())->getCasts()[$field] ?? null;
if ($cast && class_exists($cast) && is_subclass_of($cast, BaseEnum::class)) {
$searchLower = mb_strtolower($search);
return array_values(array_filter(
$cast::cases(),
fn ($item): bool => str_contains(
// @phpstan-ignore method.notFound (PhpStan cannot detect the label() method on BaseEnum cases)
2026-06-14 21:32:07 +02:00
mb_strtolower((string) $item->label()),
$searchLower
)
));
}
return $search;
}
/** @return array{sort_col:string,sort_way:\SortDirection} */
public function getSortRequest(): array
{
$rName = $this->request->route()?->getName() ?? 'noroute';
return [
'sort_col' => $this->request->string(
self::CACHE_SORT_COL_KEY,
$this->session->get("{$rName}." . self::CACHE_SORT_COL_KEY, 'undefined')
)->value(),
'sort_way' => $this->request->input(
self::CACHE_SORT_WAY_KEY,
$this->session->get("{$rName}." . self::CACHE_SORT_WAY_KEY)
) === 'desc' ? SortDirection::Descending : SortDirection::Ascending,
];
}
public function persistSortSession(string $rName, ?string $sortCol, ?SortDirection $sortWay): void
{
if ($this->request->has(self::CACHE_SORT_COL_KEY)) {
$this->session->put("{$rName}." . self::CACHE_SORT_COL_KEY, $sortCol);
}
if ($this->request->has(self::CACHE_SORT_WAY_KEY)) {
$direction = match ($sortWay) {
SortDirection::Descending => 'desc',
default => 'asc',
};
$this->session->put("{$rName}." . self::CACHE_SORT_WAY_KEY, $direction);
}
}
public function resetSortSession(string $rName): void
{
$this->session->forget([
"{$rName}." . self::CACHE_SORT_COL_KEY,
"{$rName}." . self::CACHE_SORT_WAY_KEY,
"{$rName}." . self::CACHE_SORTED_KEY,
]);
}
/**
* Helper to easily paginate and keep query parameters.
*
* @template TModel of \Illuminate\Database\Eloquent\Model
*
* @param \Illuminate\Database\Eloquent\Builder<TModel> $query
* @return \Illuminate\Pagination\LengthAwarePaginator<int, TModel>
*/
feat: implement pagination using Pagination value object - Updated DataTableService to accept a Pagination object for pagination parameters. - Refactored paginateQuery method to utilize pageSize and pageNumber from Pagination. refactor: replace ActionExecutor with Executor in controller traits - Updated ChangesModelOrder, TogglesModelStar, and TogglesModelStatus traits to use Executor interface instead of ActionExecutor. feat: add PictureMetadata and Slug value objects - Introduced PictureMetadata for managing image metadata with validation. - Added Slug value object for slug generation and validation. feat: update breadcrumbs to support RangeDto - Modified breadcrumbs-bo.php to accept RangeDto in the edit route. test: add tests for RangesController - Added tests for accessing, creating, updating, and deleting ranges. - Included tests for toggling range status. test: add integration tests for range actions - Created tests for CreateRange, DeleteRange, and UpdateRange actions. - Validated input handling and database interactions. test: add unit tests for range input classes - Implemented tests for CreateRangeInput and UpdateRangeInput to validate input constraints. test: add unit tests for value objects - Added tests for PictureMetadata and Slug to ensure proper validation and instantiation. test: add unit tests for RangeDto - Created tests to verify RangeDto mapping from Range model. test: add unit tests for RangePolicy - Implemented tests for RangePolicy to validate permissions based on user roles.
2026-09-29 16:59:07 +02:00
public function paginateQuery(\Illuminate\Database\Eloquent\Builder $query, Pagination $pagination): LengthAwarePaginator
2026-06-14 21:32:07 +02:00
{
return $query->paginate(
perPage: $pagination->pageSize()->value(),
page: $pagination->pageNumber()->value()
2026-06-14 21:32:07 +02:00
)->withQueryString();
}
}