ldc-boutique-cnc-sable/app/Services/DataTableService.php
Jérémy Munsch 3b3997d547
feat(employees): automated sync with Arcadia API, BO settings, Shopper value objects, and pagination migration
- Implement automated synchronization of employees via Arcadia API (EmployeeSyncService, SynchronizeEmployeesWithArcadiaApi, FetchAndUpsertArcadiaEmployees)
- Add Back-Office application settings for employee sites (Setting singleton, GetSettingsQuery, UpdateSettings, SettingsController, EmployeeSitesInputComponent.vue)
- Strongly type Employee attributes using Domain Value Objects (ShopperEstablishmentCode, ShopperSiteCode, ShopperServiceCode) and Eloquent Casts
- Migrate pagination from Domain to Shared\Pagination namespace across queries and inputs
- Remove legacy and dead CSV employee import/export code
- Restore Domain\Ports\Services\CsvGenerator interface for Regate order generation
- Add comprehensive Pest unit, integration, and feature test coverage
2026-10-27 09:58:44 +01:00

266 lines
8 KiB
PHP

<?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;
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);
}
/**
* 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.
*/
public function sortQuery(
string $modelClass,
Builder $query,
array $ignore = []
): 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'];
$this->persistSortSession($rName, $sort_col, $sort_way);
if ($sort_col && $sort_col !== 'undefined' && $sort_way && ! in_array($sort_col, $ignore) && $this->hasTableColumn($table, $sort_col)) {
$direction = match ($sort_way) {
SortDirection::Descending => 'desc',
default => 'asc',
};
$query->orderBy("{$table}.{$sort_col}", $direction);
} elseif ($this->hasTableColumn($table, self::ORDER_COLUMN)) {
$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
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>
*/
public function paginateQuery(\Illuminate\Database\Eloquent\Builder $query, Pagination $pagination): LengthAwarePaginator
{
return $query->paginate(
perPage: $pagination->pageSize()->value(),
page: $pagination->pageNumber()->value()
)->withQueryString();
}
}