482 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			482 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?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;
 | ||
|     }
 | ||
| } |