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 = ''; return $html; } public function setPaginationTemplate(array $tpl) { } public function getQuery(): Builder { return $this->query; } }