[], 'ranges' => [], 'dates' => [], ]; protected function resolvePreviousUrl(Request $request, string $sessionKey, ?string $fallback = null): ?string { $previousUrl = $request->get('previous_url'); if (!empty($previousUrl)) { session([$sessionKey => $previousUrl]); return $previousUrl; } $previousUrl = session($sessionKey); if (!empty($previousUrl)) { return $previousUrl; } if (!empty($fallback)) { session([$sessionKey => $fallback]); return $fallback; } return null; } protected function previousUrlForRedirect(Request $request, string $sessionKey, ?string $fallback = null): ?string { $previousUrl = $request->get('previous_url'); if (!empty($previousUrl)) { session([$sessionKey => $previousUrl]); return $previousUrl; } $previousUrl = session($sessionKey); if (!empty($previousUrl)) { return $previousUrl; } return $fallback; } /** * @param Model $model * @param string ...$columnNames * @return void */ protected function createDateFilters(Model $model, string ...$columnNames): void { foreach ($columnNames as $columnName) { $this->data['dates'][$columnName] = [ 'title' => $this->data['header'][$columnName], 'min' => DateHelper::getDateForDB($model::query()->min($columnName) ?? ''), 'max' => DateHelper::getDateForDB($model::query()->max($columnName) ?? ''), ]; } } /** * @param Model $model * @param array $columnNames * @return void */ protected function createRangeFilters(Model $model, string ...$columnNames): void { foreach ($columnNames as $columnName) { $rangeKey = $columnName; if (!isset($this->data['header'][$columnName]) && isset($this->data['header'][$columnName . '_txt'])) { $rangeKey = $columnName . '_txt'; } $title = $this->data['header'][$rangeKey] ?? $columnName; if(str_ends_with($columnName, '_price')) { $this->data['ranges'][$rangeKey] = [ 'title' => $title, 'min' => $model::query()->min($columnName) / 100, 'max' => $model::query()->max($columnName) / 100, ]; } else { $this->data['ranges'][$rangeKey] = [ 'title' => $title, 'min' => $model::query()->min($columnName), 'max' => $model::query()->max($columnName), ]; } } } /** * @param Model $model * @param array $columns * @return void */ protected function createFilters(Model $model, string ...$columns): void { foreach ($columns as $column) { $uniqueValues = $model::query()->distinct()->get($column)->pluck($column)->toArray(); foreach ($uniqueValues as $k => $v) { if(!$v) { $uniqueValues[$k] = '-пусто-'; } } $result = []; foreach ($uniqueValues as $val){ if(str_ends_with($column, '_id')) { $relation = Str::camel(str_replace('_id', '', $column)); $result[$val] = $model::query()->where($column, '=', $val)->first()?->$relation->name; } else { $result[$val] = $val; } } $this->data['filters'][$column] = [ 'title' => $this->data['header'][$column], 'values' => $result ]; } } /** * @param Model $model * @param Request $request * @return void */ protected function setSortAndOrderBy(Model $model, Request $request): void { // ------- setup sort and order -------------------------------------------------------------------------------- $this->data['sortBy'] = (!empty($request->sortBy)) ? Str::replace('_txt', '', $request->sortBy) // remove '_txt' fields modifier : $model::DEFAULT_SORT_BY ?? 'created_at'; // check for sortBy is valid field $p = new $model(); if(!in_array($this->data['sortBy'], array_merge(['id', 'created_at'], $p->getFillable()))) { $this->data['sortBy'] = $model::DEFAULT_SORT_BY ?? 'created_at'; } $allowedPerPage = [50, 100, 200, 500, 2000]; $requestedPerPage = (int)$request->input('per_page', 0); $sessionPerPage = (int)session('per_page', 0); $defaultPerPage = (int)config('pagination.per_page'); if(in_array($requestedPerPage, $allowedPerPage, true)) { $this->data['per_page'] = $requestedPerPage; } elseif(in_array($sessionPerPage, $allowedPerPage, true)) { $this->data['per_page'] = $sessionPerPage; } elseif(in_array($defaultPerPage, $allowedPerPage, true)) { $this->data['per_page'] = $defaultPerPage; } else { $this->data['per_page'] = 50; } session(['per_page' => $this->data['per_page']]); // set order if ($request->has('order')) { $this->data['orderBy'] = empty($request->order) ? 'asc' : 'desc'; } else { $this->data['orderBy'] = defined($model::class . '::DEFAULT_ORDER_BY') ? $model::DEFAULT_ORDER_BY : 'asc'; } } protected function applyStableSorting(Builder $query, string $fallbackSortBy = 'id'): void { $sortBy = $this->data['sortBy'] ?? 'created_at'; $orderBy = $this->data['orderBy'] ?? 'asc'; $query->orderBy($sortBy, $orderBy); if($sortBy !== $fallbackSortBy) { $query->orderBy($fallbackSortBy, $orderBy); } } /** * @param Builder $query * @param Request $request * @return void */ protected function acceptFilters(Builder $query, Request $request): void { // accept filters if(!empty($request->filters) && is_array($request->filters)) { // Собираем имена range и date фильтров для пропуска _from/_to в основном цикле $rangeKeys = array_keys($this->data['ranges'] ?? []); $dateKeys = array_keys($this->data['dates'] ?? []); $rangeAndDateKeys = array_merge($rangeKeys, $dateKeys); foreach ($request->filters as $filterName => $filterValue) { if(!$filterValue && $filterValue !== '0') continue; // Пропускаем _from/_to — они обрабатываются ниже if(Str::endsWith($filterName, ['_from', '_to'])) { continue; } // Резолвим виртуальные столбцы и значения [$dbColumn, $dbValue] = $this->resolveFilterColumn($filterName, $filterValue); if(Str::contains($dbValue, '||')) { $values = explode('||', $dbValue); $query->where(function ($q) use ($dbColumn, $values) { $nonNullValues = []; foreach ($values as $v) { if ($v == '-пусто-') { $q->orWhereNull($dbColumn); } else { $nonNullValues[] = $v; } } if (!empty($nonNullValues)) { $q->orWhereIn($dbColumn, $nonNullValues); } }); } else { if($dbValue == '-пусто-') { $query->whereNull($dbColumn); } else { $query->where($dbColumn, $dbValue); } } } // Обработка range-фильтров (_from / _to) foreach ($rangeAndDateKeys as $columnName) { $fromValue = $request->filters[$columnName . '_from'] ?? null; $toValue = $request->filters[$columnName . '_to'] ?? null; // Для price-полей значения в форме в рублях, в БД в копейках $dbColumn = str_ends_with($columnName, '_txt') ? substr($columnName, 0, -4) : $columnName; $multiplier = str_ends_with($dbColumn, '_price') ? 100 : 1; if($fromValue !== null && $fromValue !== '') { $query->where($dbColumn, '>=', $fromValue * $multiplier); } if($toValue !== null && $toValue !== '') { $query->where($dbColumn, '<=', $toValue * $multiplier); } } } } /** * Резолвит виртуальные имена столбцов и маппит отображаемые значения обратно на значения БД. * * @return array{0: string, 1: string} [реальный_столбец, значение_для_бд] */ protected function resolveFilterColumn(string $filterName, string $filterValue): array { $table = $this->data['id'] ?? ''; $columnMap = FilterController::COLUMN_MAP[$table] ?? []; $valueMap = FilterController::VALUE_MAP[$table] ?? []; // Резолвим имя столбца: явный маппинг → убираем _txt if (isset($columnMap[$filterName])) { $dbColumn = $columnMap[$filterName]; } elseif (str_ends_with($filterName, '_txt')) { $dbColumn = substr($filterName, 0, -4); } else { $dbColumn = $filterName; } // Обратный маппинг значений if (isset($valueMap[$dbColumn])) { $reverseMap = array_flip($valueMap[$dbColumn]); $parts = explode('||', $filterValue); $mapped = array_map(fn($v) => (string)($reverseMap[$v] ?? $v), $parts); $filterValue = implode('||', $mapped); } // Для цен: значения в рублях, в БД в копейках if (str_ends_with($dbColumn, '_price')) { $parts = explode('||', $filterValue); $mapped = array_map(function ($v) { if ($v === '-пусто-') { return $v; } $normalized = str_replace(',', '.', (string)$v); if (!is_numeric($normalized)) { return $v; } return (string) round(((float)$normalized) * 100); }, $parts); $filterValue = implode('||', $mapped); } return [$dbColumn, $filterValue]; } /** * @param Builder $query * @param Request $request * @return void */ protected function acceptSearch(Builder $query, Request $request): void { // accept search if(!empty($request->s)) { $s = $request->s; $searchFields = $this->data['searchFields']; $query->where(function ($query) use ($searchFields, $s) { foreach ($searchFields as $searchField) { if(Str::contains($searchField, '-')) { list($relation, $column) = explode('-', $searchField); $query->orWhereHas($relation, function ($query) use ($s, $column) { $query->where($column, 'LIKE', "%{$s}%"); }); } else { $query->orWhere($searchField, 'LIKE', '%' . $s . '%'); } } }); } } }