315 lines
12 KiB
PHP
315 lines
12 KiB
PHP
<?php
|
||
|
||
namespace App\Controllers;
|
||
|
||
use CodeIgniter\Controller;
|
||
use CodeIgniter\HTTP\RequestInterface;
|
||
use CodeIgniter\HTTP\ResponseInterface;
|
||
use Psr\Log\LoggerInterface;
|
||
use App\Models\OrganizationModel;
|
||
use App\Services\AccessService;
|
||
|
||
/**
|
||
* BaseController provides a convenient place for loading components
|
||
* and performing functions that are needed by all your controllers.
|
||
*
|
||
* Extend this class in any new controllers:
|
||
* ```
|
||
* class Home extends BaseController
|
||
* ```
|
||
*
|
||
* For security, be sure to declare any new methods as protected or private.
|
||
*/
|
||
abstract class BaseController extends Controller
|
||
{
|
||
/**
|
||
* Be sure to declare properties for any property fetch you initialized.
|
||
* The creation of dynamic property is deprecated in PHP 8.2.
|
||
*/
|
||
|
||
protected $session;
|
||
protected AccessService $access;
|
||
|
||
/**
|
||
* @return void
|
||
*/
|
||
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger)
|
||
{
|
||
// Load here all helpers you want to be available in your controllers that extend BaseController.
|
||
// Caution: Do not put the this below the parent::initController() call below.
|
||
// $this->helpers = ['form', 'url'];
|
||
|
||
// Caution: Do not edit this line.
|
||
parent::initController($request, $response, $logger);
|
||
|
||
// Preload any models, libraries, etc, here.
|
||
$this->session = service('session');
|
||
$this->access = service('access');
|
||
|
||
// Загружаем хелпер доступа для Twig
|
||
helper('access');
|
||
}
|
||
|
||
/**
|
||
* Проверка права на действие ( shortcut для $this->access->can() )
|
||
*
|
||
* @param string $action
|
||
* @param string $resource
|
||
* @return bool
|
||
*/
|
||
protected function can(string $action, string $resource): bool
|
||
{
|
||
return $this->access->can($action, $resource);
|
||
}
|
||
|
||
/**
|
||
* Проверка роли (shortcut для $this->access->isRole() )
|
||
*
|
||
* @param string|array $roles
|
||
* @return bool
|
||
*/
|
||
protected function isRole($roles): bool
|
||
{
|
||
return $this->access->isRole($roles);
|
||
}
|
||
|
||
public function renderTwig($template, $data = [])
|
||
{
|
||
helper('csrf');
|
||
|
||
$twig = \Config\Services::twig();
|
||
|
||
// oldInput из сессии добавляется в данные шаблона
|
||
// Расширение TwigGlobalsExtension автоматически добавляет session, alerts, old, currentOrg
|
||
$oldInput = $this->session->get('_ci_old_input') ?? [];
|
||
$data['old'] = $data['old'] ?? $oldInput;
|
||
|
||
// Добавляем access в данные шаблона для функций can(), isRole() и т.д.
|
||
$data['access'] = $this->access;
|
||
|
||
ob_start();
|
||
$twig->display($template, $data);
|
||
$content = ob_get_clean();
|
||
|
||
return $content;
|
||
}
|
||
|
||
// ========================================
|
||
// Методы для универсальных таблиц
|
||
// ========================================
|
||
|
||
/**
|
||
* Конфигурация таблицы - переопределяется в каждом контроллере
|
||
*/
|
||
protected function getTableConfig(): array
|
||
{
|
||
return [
|
||
'model' => null,
|
||
'columns' => [],
|
||
'searchable' => [],
|
||
'sortable' => [],
|
||
'defaultSort' => 'id',
|
||
'order' => 'asc',
|
||
'itemsKey' => 'items',
|
||
'scope' => null, // callable($builder) для дополнительных модификаций
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Проверка AJAX запроса
|
||
*/
|
||
protected function isAjax(): bool
|
||
{
|
||
$header = $this->request->header('X-Requested-With');
|
||
$value = $header ? $header->getValue() : '';
|
||
return strtolower($value) === 'xmlhttprequest';
|
||
}
|
||
|
||
/**
|
||
* Подготовка данных таблицы (общая логика для всех таблиц)
|
||
*/
|
||
protected function prepareTableData(?array $config = null): array
|
||
{
|
||
$config = array_merge($this->getTableConfig(), $config ?? []);
|
||
$page = (int) ($this->request->getGet('page') ?? 1);
|
||
$perPage = (int) ($this->request->getGet('perPage') ?? 10);
|
||
$sort = $this->request->getGet('sort') ?? $config['defaultSort'];
|
||
$order = $this->request->getGet('order') ?? $config['order'];
|
||
|
||
// Исправление: получаем фильтры из параметра filters[]
|
||
$filters = [];
|
||
$rawFilters = $this->request->getGet('filters');
|
||
|
||
if ($rawFilters) {
|
||
if (is_array($rawFilters)) {
|
||
$filters = $rawFilters;
|
||
} else {
|
||
// Для обратной совместимости, если фильтры пришли в строке
|
||
parse_str($rawFilters, $filters);
|
||
if (isset($filters['filters'])) {
|
||
$filters = $filters['filters'];
|
||
}
|
||
}
|
||
} else {
|
||
// Старый способ извлечения фильтров для совместимости
|
||
foreach ($this->request->getGet() as $key => $value) {
|
||
if (str_starts_with($key, 'filters[') && str_ends_with($key, ']')) {
|
||
$field = substr($key, 8, -1); // Исправлено: было 9, должно быть 8
|
||
$filters[$field] = $value;
|
||
}
|
||
}
|
||
}
|
||
|
||
$model = $config['model'];
|
||
|
||
// Если есть кастомный scope - создаём новый чистый запрос
|
||
// scope будет полностью контролировать FROM, JOIN, SELECT
|
||
if (isset($config['scope']) && is_callable($config['scope'])) {
|
||
$builder = $model->db()->newQuery();
|
||
$config['scope']($builder);
|
||
} else {
|
||
// Стандартный путь - используем builder модели
|
||
$builder = $model->builder();
|
||
$builder->resetQuery();
|
||
|
||
// Автоматическая фильтрация по организации для моделей с TenantScopedModel
|
||
$modelClass = get_class($model);
|
||
$traits = class_uses($modelClass);
|
||
if (in_array('App\Models\Traits\TenantScopedModel', $traits)) {
|
||
$model->forCurrentOrg();
|
||
}
|
||
}
|
||
|
||
// Применяем фильтры
|
||
foreach ($filters as $filterKey => $value) {
|
||
if ($value === '') {
|
||
continue;
|
||
}
|
||
|
||
// Сначала проверяем fieldMap (алиасы) — они имеют приоритет
|
||
if (isset($config['fieldMap']) && isset($config['fieldMap'][$filterKey])) {
|
||
$realField = $config['fieldMap'][$filterKey];
|
||
$builder->like($realField, $value);
|
||
}
|
||
// Потом проверяем прямое совпадение
|
||
elseif (in_array($filterKey, $config['searchable'])) {
|
||
$builder->like($filterKey, $value);
|
||
}
|
||
}
|
||
|
||
// Сортировка
|
||
if ($sort && in_array($sort, $config['sortable'])) {
|
||
$builder->orderBy($sort, $order);
|
||
}
|
||
|
||
// Сохраняем текущее состояние builder для подсчета
|
||
$countBuilder = clone $builder;
|
||
$total = $countBuilder->countAllResults(false);
|
||
|
||
// Получаем данные с пагинацией (scope уже установил нужный SELECT)
|
||
$items = $builder->limit($perPage, ($page - 1) * $perPage)->get()->getResultArray();
|
||
|
||
$from = ($page - 1) * $perPage + 1;
|
||
$to = min($page * $perPage, $total);
|
||
|
||
$pagerData = [
|
||
'currentPage' => $page,
|
||
'pageCount' => $total > 0 ? (int) ceil($total / $perPage) : 1,
|
||
'total' => $total,
|
||
'perPage' => $perPage,
|
||
'from' => $from,
|
||
'to' => $to,
|
||
];
|
||
|
||
$data = [
|
||
'items' => $items, // Алиас для универсального шаблона
|
||
'pagerDetails' => $pagerData,
|
||
'perPage' => $perPage,
|
||
'sort' => $sort,
|
||
'order' => $order,
|
||
'filters' => $filters,
|
||
'columns' => $config['columns'],
|
||
'actionsConfig' => $config['actionsConfig'] ?? [],
|
||
'can_edit' => $config['can_edit'] ?? true,
|
||
'can_delete' => $config['can_delete'] ?? true,
|
||
];
|
||
|
||
return $data;
|
||
}
|
||
|
||
/**
|
||
* Рендерит HTML таблицы из конфигурации
|
||
*
|
||
* @param array|null $config Конфигурация таблицы (если null, используется getTableConfig())
|
||
* @param bool $isPartial Если true, возвращает только tbody + tfoot (для AJAX)
|
||
* @return string HTML таблицы
|
||
*/
|
||
protected function renderTable(?array $config = null, bool $isPartial = false): string
|
||
{
|
||
$config = $config ?? $this->getTableConfig();
|
||
$tableData = $this->prepareTableData($config);
|
||
|
||
// Дополнительные параметры для компонента таблицы
|
||
$tableData['id'] = $config['id'] ?? 'data-table';
|
||
$tableData['url'] = $config['url'] ?? '/table';
|
||
$tableData['perPage'] = $tableData['perPage'] ?? 10;
|
||
$tableData['sort'] = $tableData['sort'] ?? '';
|
||
$tableData['order'] = $tableData['order'] ?? 'asc';
|
||
$tableData['filters'] = $tableData['filters'] ?? [];
|
||
$tableData['actions'] = $config['actions'] ?? false;
|
||
$tableData['actionsConfig'] = $config['actionsConfig'] ?? [];
|
||
$tableData['columns'] = $config['columns'] ?? [];
|
||
|
||
// Параметры для пустого состояния
|
||
$tableData['emptyMessage'] = $config['emptyMessage'] ?? 'Нет данных';
|
||
$tableData['emptyIcon'] = $config['emptyIcon'] ?? '';
|
||
$tableData['emptyActionUrl'] = $config['emptyActionUrl'] ?? '';
|
||
$tableData['emptyActionLabel'] = $config['emptyActionLabel'] ?? 'Добавить';
|
||
$tableData['emptyActionIcon'] = $config['emptyActionIcon'] ?? '';
|
||
|
||
$template = $isPartial ? '@components/table/ajax_table' : '@components/table/table';
|
||
|
||
return $this->renderTwig($template, $tableData);
|
||
}
|
||
|
||
/**
|
||
* AJAX endpoint для таблицы
|
||
*
|
||
* Логика:
|
||
* - Если format=partial или AJAX → возвращает только tbody + tfoot
|
||
* - Если прямой GET → редиректит на основную страницу с теми же параметрами
|
||
*
|
||
* @param array|null $config Кастомная конфигурация таблицы (если null, используется getTableConfig())
|
||
* @param string|null $pageUrl URL основной страницы таблицы (для редиректа)
|
||
* @return string|ResponseInterface
|
||
*/
|
||
public function table(?array $config = null, ?string $pageUrl = null)
|
||
{
|
||
$isPartial = $this->request->getGet('format') === 'partial' || $this->isAjax();
|
||
|
||
// Если это частичный запрос (AJAX) — возвращаем только таблицу
|
||
if ($isPartial) {
|
||
return $this->renderTable($config, true);
|
||
}
|
||
|
||
// Прямой запрос к /table — редиректим на основную страницу
|
||
// Сохраняем все параметры: page, perPage, sort, order, filters
|
||
$params = $this->request->getGet();
|
||
unset($params['format']); // Убираем format=partial если был
|
||
|
||
if ($pageUrl) {
|
||
$redirectUrl = $pageUrl;
|
||
} else {
|
||
// Пытаемся извлечь URL из config['url']
|
||
$tableUrl = $config['url'] ?? '/table';
|
||
$redirectUrl = $tableUrl;
|
||
}
|
||
|
||
if (!empty($params)) {
|
||
$redirectUrl .= '?' . http_build_query($params);
|
||
}
|
||
|
||
return redirect()->to($redirectUrl);
|
||
}
|
||
}
|