Files
gestalt/kernel/EloquentDataProvider.php
2025-06-18 14:50:18 +03:00

482 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace kernel;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
class EloquentDataProvider
{
protected Builder $query;
protected int $perPage = 15;
protected array $filters = [];
protected array $sort = [];
protected array $with = [];
protected array $withCount = [];
protected array $search = [];
protected array $allowedFilters = [];
protected array $allowedSorts = [];
protected array $allowedSearch = [];
protected array $defaultSort = [];
protected array $beforeQueryCallbacks = [];
protected array $afterQueryCallbacks = [];
protected array $paginationTemplates = [];
public function __construct(Builder $query)
{
$this->query = $query;
}
public function setPerPage(int $perPage): self
{
$this->perPage = max(1, $perPage);
return $this;
}
public function setFilters(array $filters): self
{
$this->filters = $filters;
return $this;
}
public function setSort(array $sort): self
{
$this->sort = $sort;
return $this;
}
public function with(array $relations): self
{
$this->with = $relations;
return $this;
}
public function withCount(array $relations): self
{
$this->withCount = $relations;
return $this;
}
public function setSearch(array $search): self
{
$this->search = $search;
return $this;
}
public function allowFilters(array $filters): self
{
$this->allowedFilters = $filters;
return $this;
}
public function allowSorts(array $sorts): self
{
$this->allowedSorts = $sorts;
return $this;
}
public function allowSearch(array $fields): self
{
$this->allowedSearch = $fields;
return $this;
}
public function setDefaultSort(array $sort): self
{
$this->defaultSort = $sort;
return $this;
}
public function beforeQuery(callable $callback): self
{
$this->beforeQueryCallbacks[] = $callback;
return $this;
}
public function afterQuery(callable $callback): self
{
$this->afterQueryCallbacks[] = $callback;
return $this;
}
protected function applyFilters(): void
{
foreach ($this->filters as $field => $value) {
if (!$this->isFilterAllowed($field)) {
continue;
}
if (is_array($value)) {
$this->applyArrayFilter($field, $value);
} elseif (Str::contains($field, '.')) {
$this->applyRelationFilter($field, $value);
} elseif ($value !== null && $value !== '') {
$this->query->where($field, $value);
}
}
}
protected function applyArrayFilter(string $field, array $value): void
{
$operator = strtolower($value[0] ?? null);
$operand = $value[1] ?? null;
switch ($operator) {
case 'in':
$this->query->whereIn($field, (array)$operand);
break;
case 'not in':
$this->query->whereNotIn($field, (array)$operand);
break;
case 'between':
$this->query->whereBetween($field, (array)$operand);
break;
case 'not between':
$this->query->whereNotBetween($field, (array)$operand);
break;
case 'null':
$this->query->whereNull($field);
break;
case 'not null':
$this->query->whereNotNull($field);
break;
case 'like':
$this->query->where($field, 'like', "%{$operand}%");
break;
case '>':
case '<':
case '>=':
case '<=':
case '!=':
$this->query->where($field, $operator, $operand);
break;
default:
$this->query->whereIn($field, $value);
}
}
protected function applyRelationFilter(string $field, $value): void
{
[$relation, $column] = explode('.', $field, 2);
$this->query->whereHas($relation, function ($query) use ($column, $value) {
if (is_array($value)) {
$query->whereIn($column, $value);
} else {
$query->where($column, $value);
}
});
}
protected function applySort(): void
{
if (empty($this->sort) && !empty($this->defaultSort)) {
$this->sort = $this->defaultSort;
}
foreach ($this->sort as $field => $direction) {
if (!$this->isSortAllowed($field)) {
continue;
}
$direction = strtolower($direction) === 'desc' ? 'desc' : 'asc';
if (Str::contains($field, '.')) {
$this->applyRelationSort($field, $direction);
} else {
$this->query->orderBy($field, $direction);
}
}
}
protected function applyRelationSort(string $field, string $direction): void
{
[$relation, $column] = explode('.', $field, 2);
$this->query->with([$relation => function ($query) use ($column, $direction) {
$query->orderBy($column, $direction);
}]);
}
protected function applySearch(): void
{
if (empty($this->search) || empty($this->allowedSearch)) {
return;
}
$searchTerm = Arr::get($this->search, 'term', '');
if (empty($searchTerm)) {
return;
}
$this->query->where(function ($query) use ($searchTerm) {
foreach ($this->allowedSearch as $field) {
if (Str::contains($field, '.')) {
[$relation, $column] = explode('.', $field, 2);
$query->orWhereHas($relation, function ($q) use ($column, $searchTerm) {
$q->where($column, 'like', "%{$searchTerm}%");
});
} else {
$query->orWhere($field, 'like', "%{$searchTerm}%");
}
}
});
}
protected function applyRelations(): void
{
if (!empty($this->with)) {
$this->query->with($this->with);
}
if (!empty($this->withCount)) {
$this->query->withCount($this->withCount);
}
}
protected function isFilterAllowed(string $field): bool
{
if (empty($this->allowedFilters)) {
return true;
}
$baseField = Str::before($field, '.');
return in_array($field, $this->allowedFilters) ||
in_array($baseField, $this->allowedFilters);
}
protected function isSortAllowed(string $field): bool
{
if (empty($this->allowedSorts)) {
return true;
}
$baseField = Str::before($field, '.');
return in_array($field, $this->allowedSorts) ||
in_array($baseField, $this->allowedSorts);
}
protected function executeCallbacks(array $callbacks): void
{
foreach ($callbacks as $callback) {
call_user_func($callback, $this->query);
}
}
/**
* Получение данных с ручной пагинацией
*/
public function getManualPaginated(int $page = 1, int $perPage = null): array
{
$perPage = $perPage ?? $this->perPage;
$this->applyRelations();
$this->applyFilters();
$this->applySearch();
$this->applySort();
$total = $this->query->count();
$results = $this->query
->offset(($page - 1) * $perPage)
->limit($perPage)
->get();
return [
'data' => $results,
'meta' => [
'total' => $total,
'per_page' => $perPage,
'current_page' => $page,
'last_page' => ceil($total / $perPage),
]
];
}
public function getAll(): \Illuminate\Database\Eloquent\Collection
{
$this->executeCallbacks($this->beforeQueryCallbacks);
$this->applyRelations();
$this->applyFilters();
$this->applySearch();
$this->applySort();
$result = $this->query->get();
$this->executeCallbacks($this->afterQueryCallbacks);
return $result;
}
public function getFirst(): ?\Illuminate\Database\Eloquent\Model
{
$this->executeCallbacks($this->beforeQueryCallbacks);
$this->applyRelations();
$this->applyFilters();
$this->applySearch();
$this->applySort();
$result = $this->query->first();
$this->executeCallbacks($this->afterQueryCallbacks);
return $result;
}
/**
* Генерирует массив с основными ссылками пагинации
*
* @param array $meta Мета-информация из getManualPaginated()
* @param string|null $baseUrl Базовый URL (если null - определит автоматически)
* @return array
*/
public function getPaginationLinks(array $meta, ?string $baseUrl = null): array
{
$currentPage = $meta['current_page'];
$lastPage = $meta['last_page'];
// Определяем базовый URL
$baseUrl = $this->normalizeBaseUrl($baseUrl);
$links = [
'first' => null,
'previous' => null,
'current' => $this->buildPageUrl($baseUrl, $currentPage),
'next' => null,
'last' => null,
];
// Первая страница (если не на первой)
if ($currentPage > 1) {
$links['first'] = $this->buildPageUrl($baseUrl, 1);
}
// Предыдущая страница
if ($currentPage > 1) {
$links['previous'] = $this->buildPageUrl($baseUrl, $currentPage - 1);
}
// Следующая страница
if ($currentPage < $lastPage) {
$links['next'] = $this->buildPageUrl($baseUrl, $currentPage + 1);
}
// Последняя страница (если не на последней)
if ($currentPage < $lastPage) {
$links['last'] = $this->buildPageUrl($baseUrl, $lastPage);
}
// Дополнительная мета-информация
$links['meta'] = [
'current_page' => $currentPage,
'last_page' => $lastPage,
'per_page' => $meta['per_page'],
'total' => $meta['total']
];
return $links;
}
/**
* Нормализует базовый URL
*/
protected function normalizeBaseUrl(?string $url): string
{
if ($url !== null) {
return $url;
}
if (function_exists('url')) {
// Laravel
return url()->current();
}
// Чистый PHP
$protocol = ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || ($_SERVER['SERVER_PORT'] ?? null) == 443) ? 'https://' : 'http://';
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
$uri = $_SERVER['REQUEST_URI'] ?? '/';
// Удаляем параметр page если он есть
$uri = preg_replace('/([?&])page=[^&]*(&|$)/', '$1', $uri);
return $protocol . $host . rtrim($uri, '?&');
}
/**
* Строит URL для конкретной страницы
*/
protected function buildPageUrl(string $baseUrl, int $page): string
{
if ($page <= 1) {
return $baseUrl; // Первая страница без параметра
}
$separator = strpos($baseUrl, '?') === false ? '?' : '&';
return $baseUrl . $separator . 'page=' . $page;
}
public function renderPaginationLinks(array $meta, string $url, int $showPages = 5): string
{
$currentPage = $meta['current_page'];
$lastPage = $meta['last_page'];
// Определяем диапазон страниц для отображения
$startPage = max(1, $currentPage - floor($showPages / 2));
$endPage = min($lastPage, $startPage + $showPages - 1);
$html = '<div class="pagination">';
// Кнопка "Назад"
if ($currentPage > 1) {
$html .= '<a href="' . $this->buildPageUrl($url, $currentPage - 1) . '" class="page-link prev">« Назад</a>';
}
// Первая страница
if ($startPage > 1) {
$html .= '<a href="' . $this->buildPageUrl($url, 1) . '" class="page-link">1</a>';
if ($startPage > 2) {
$html .= '<span class="page-dots">...</span>';
}
}
// Основные страницы
for ($i = $startPage; $i <= $endPage; $i++) {
$activeClass = $i == $currentPage ? ' active' : '';
$html .= '<a href="' . $this->buildPageUrl($url, $i) . '" class="page-link' . $activeClass . '">' . $i . '</a>';
}
// Последняя страница
if ($endPage < $lastPage) {
if ($endPage < $lastPage - 1) {
$html .= '<span class="page-dots">...</span>';
}
$html .= '<a href="' . $this->buildPageUrl($url, $lastPage) . '" class="page-link">' . $lastPage . '</a>';
}
// Кнопка "Вперед"
if ($currentPage < $lastPage) {
$html .= '<a href="' . $this->buildPageUrl($url, $currentPage + 1) . '" class="page-link next">Вперед »</a>';
}
$html .= '</div>';
return $html;
}
public function setPaginationTemplate(array $tpl)
{
}
public function getQuery(): Builder
{
return $this->query;
}
}