514 lines
19 KiB
PHP
514 lines
19 KiB
PHP
<?php
|
||
|
||
namespace App\Libraries\Twig;
|
||
|
||
use Twig\Extension\AbstractExtension;
|
||
use Twig\TwigFunction;
|
||
use Twig\TwigGlobal;
|
||
use App\Models\OrganizationModel;
|
||
use Config\Services;
|
||
|
||
class TwigGlobalsExtension extends AbstractExtension
|
||
{
|
||
public function getFunctions(): array
|
||
{
|
||
return [
|
||
new TwigFunction('get_session', [$this, 'getSession'], ['is_safe' => ['html']]),
|
||
new TwigFunction('get_current_org', [$this, 'getCurrentOrg'], ['is_safe' => ['html']]),
|
||
new TwigFunction('get_alerts', [$this, 'getAlerts'], ['is_safe' => ['html']]),
|
||
new TwigFunction('render_pager', [$this, 'renderPager'], ['is_safe' => ['html']]),
|
||
new TwigFunction('is_active_route', [$this, 'isActiveRoute'], ['is_safe' => ['html']]),
|
||
new TwigFunction('get_current_route', [$this, 'getCurrentRoute'], ['is_safe' => ['html']]),
|
||
new TwigFunction('render_actions', [$this, 'renderActions'], ['is_safe' => ['html']]),
|
||
new TwigFunction('render_cell', [$this, 'renderCell'], ['is_safe' => ['html']]),
|
||
new TwigFunction('get_avatar_url', [$this, 'getAvatarUrl'], ['is_safe' => ['html']]),
|
||
new TwigFunction('get_avatar', [$this, 'getAvatar'], ['is_safe' => ['html']]),
|
||
|
||
// Access functions
|
||
new TwigFunction('can', [$this, 'can'], ['is_safe' => ['html']]),
|
||
new TwigFunction('is_role', [$this, 'isRole'], ['is_safe' => ['html']]),
|
||
new TwigFunction('is_owner', [$this, 'isOwner'], ['is_safe' => ['html']]),
|
||
new TwigFunction('is_admin', [$this, 'isAdmin'], ['is_safe' => ['html']]),
|
||
new TwigFunction('is_manager', [$this, 'isManager'], ['is_safe' => ['html']]),
|
||
new TwigFunction('current_role', [$this, 'currentRole'], ['is_safe' => ['html']]),
|
||
new TwigFunction('role_label', [$this, 'roleLabel'], ['is_safe' => ['html']]),
|
||
new TwigFunction('can_view', [$this, 'canView'], ['is_safe' => ['html']]),
|
||
new TwigFunction('can_create', [$this, 'canCreate'], ['is_safe' => ['html']]),
|
||
new TwigFunction('can_edit', [$this, 'canEdit'], ['is_safe' => ['html']]),
|
||
new TwigFunction('can_delete', [$this, 'canDelete'], ['is_safe' => ['html']]),
|
||
new TwigFunction('can_manage_users', [$this, 'canManageUsers'], ['is_safe' => ['html']]),
|
||
|
||
// Role & Status badge functions
|
||
new TwigFunction('role_badge', [$this, 'roleBadge'], ['is_safe' => ['html']]),
|
||
new TwigFunction('status_badge', [$this, 'statusBadge'], ['is_safe' => ['html']]),
|
||
new TwigFunction('get_all_roles', [$this, 'getAllRoles'], ['is_safe' => ['html']]),
|
||
];
|
||
}
|
||
|
||
// ========================================
|
||
// Access Functions для Twig
|
||
// ========================================
|
||
|
||
public function can(string $action, string $resource): bool
|
||
{
|
||
return service('access')->can($action, $resource);
|
||
}
|
||
|
||
public function isRole($roles): bool
|
||
{
|
||
return service('access')->isRole($roles);
|
||
}
|
||
|
||
public function isOwner(): bool
|
||
{
|
||
return service('access')->isRole(\App\Services\AccessService::ROLE_OWNER);
|
||
}
|
||
|
||
public function isAdmin(): bool
|
||
{
|
||
$role = service('access')->getCurrentRole();
|
||
return $role === \App\Services\AccessService::ROLE_ADMIN
|
||
|| $role === \App\Services\AccessService::ROLE_OWNER;
|
||
}
|
||
|
||
public function isManager(): bool
|
||
{
|
||
return service('access')->isManagerOrHigher();
|
||
}
|
||
|
||
public function currentRole(): ?string
|
||
{
|
||
return service('access')->getCurrentRole();
|
||
}
|
||
|
||
public function roleLabel(string $role): string
|
||
{
|
||
return service('access')->getRoleLabel($role);
|
||
}
|
||
|
||
public function canView(string $resource): bool
|
||
{
|
||
return service('access')->can(\App\Services\AccessService::PERMISSION_VIEW, $resource);
|
||
}
|
||
|
||
public function canCreate(string $resource): bool
|
||
{
|
||
return service('access')->can(\App\Services\AccessService::PERMISSION_CREATE, $resource);
|
||
}
|
||
|
||
public function canEdit(string $resource): bool
|
||
{
|
||
return service('access')->can(\App\Services\AccessService::PERMISSION_EDIT, $resource);
|
||
}
|
||
|
||
public function canDelete(string $resource): bool
|
||
{
|
||
return service('access')->can(\App\Services\AccessService::PERMISSION_DELETE, $resource);
|
||
}
|
||
|
||
public function canManageUsers(): bool
|
||
{
|
||
return service('access')->canManageUsers();
|
||
}
|
||
|
||
// ========================================
|
||
// Role & Status Badge Functions
|
||
// ========================================
|
||
|
||
public function roleBadge(string $role): string
|
||
{
|
||
$colors = [
|
||
'owner' => 'bg-primary',
|
||
'admin' => 'bg-info',
|
||
'manager' => 'bg-success',
|
||
'guest' => 'bg-secondary',
|
||
];
|
||
|
||
$labels = [
|
||
'owner' => 'Владелец',
|
||
'admin' => 'Администратор',
|
||
'manager' => 'Менеджер',
|
||
'guest' => 'Гость',
|
||
];
|
||
|
||
$color = $colors[$role] ?? 'bg-secondary';
|
||
$label = $labels[$role] ?? $role;
|
||
|
||
return '<span class="badge ' . esc($color) . '">' . esc($label) . '</span>';
|
||
}
|
||
|
||
public function statusBadge(string $status): string
|
||
{
|
||
$colors = [
|
||
'active' => 'bg-success',
|
||
'pending' => 'bg-warning text-dark',
|
||
'blocked' => 'bg-danger',
|
||
];
|
||
|
||
$labels = [
|
||
'active' => 'Активен',
|
||
'pending' => 'Ожидает',
|
||
'blocked' => 'Заблокирован',
|
||
];
|
||
|
||
$color = $colors[$status] ?? 'bg-secondary';
|
||
$label = $labels[$status] ?? $status;
|
||
|
||
return '<span class="badge ' . esc($color) . '">' . esc($label) . '</span>';
|
||
}
|
||
|
||
public function getAvatarUrl($avatar = null, $size = 32): string
|
||
{
|
||
if (empty($avatar)) {
|
||
return '';
|
||
}
|
||
return base_url('/uploads/avatars/' . $avatar);
|
||
}
|
||
|
||
public function getAvatar($user = null, $size = 32, $class = ''): string
|
||
{
|
||
if (!$user) {
|
||
$session = session();
|
||
$userId = $session->get('user_id');
|
||
if (!$userId) {
|
||
return '';
|
||
}
|
||
$userModel = new \App\Models\UserModel();
|
||
$user = $userModel->find($userId);
|
||
}
|
||
|
||
$name = $user['name'] ?? 'U';
|
||
$avatar = $user['avatar'] ?? null;
|
||
|
||
if ($avatar) {
|
||
$url = base_url('/uploads/avatars/' . $avatar);
|
||
$style = "width: {$size}px; height: {$size}px; object-fit: cover; border-radius: 50%;";
|
||
return '<img src="' . esc($url) . '" alt="' . esc($name) . '" class="' . esc($class) . '" style="' . esc($style) . '">';
|
||
}
|
||
|
||
// Генерируем фон на основе имени
|
||
$colors = ['667eea', '764ba2', 'f093fb', 'f5576c', '4facfe', '00f2fe'];
|
||
$color = $colors[crc32($name) % count($colors)];
|
||
$initial = strtoupper(substr($name, 0, 1));
|
||
$style = "width: {$size}px; height: {$size}px; background: linear-gradient(135deg, #{$color} 0%, #{$color}dd 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: " . ($size / 2) . "px;";
|
||
|
||
return '<div class="' . esc($class) . '" style="' . esc($style) . '">' . esc($initial) . '</div>';
|
||
}
|
||
|
||
public function getAllRoles(): array
|
||
{
|
||
return \App\Services\AccessService::getAllRoles();
|
||
}
|
||
|
||
public function getSession()
|
||
{
|
||
return session();
|
||
}
|
||
|
||
|
||
public function getCurrentOrg()
|
||
{
|
||
$session = session();
|
||
$activeOrgId = $session->get('active_org_id');
|
||
|
||
if ($activeOrgId) {
|
||
$orgModel = new OrganizationModel();
|
||
return $orgModel->find($activeOrgId);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
public function getAlerts(): array
|
||
{
|
||
$session = session();
|
||
|
||
$alerts = [];
|
||
$types = ['success', 'error', 'warning', 'info'];
|
||
foreach ($types as $type) {
|
||
if ($msg = $session->getFlashdata($type)) {
|
||
$alerts[] = ['type' => $type, 'message' => $msg];
|
||
}
|
||
}
|
||
|
||
if ($validationErrors = $session->getFlashdata('errors')) {
|
||
foreach ($validationErrors as $error) {
|
||
$alerts[] = ['type' => 'error', 'message' => $error];
|
||
}
|
||
}
|
||
|
||
return $alerts;
|
||
}
|
||
|
||
public function renderPager($pager)
|
||
{
|
||
if (!$pager) {
|
||
return '';
|
||
}
|
||
|
||
return $pager->links();
|
||
}
|
||
|
||
/**
|
||
* Проверяет, является ли текущий маршрут активным
|
||
*/
|
||
public function isActiveRoute($routes, $exact = false): bool
|
||
{
|
||
$currentRoute = $this->getCurrentRoute();
|
||
|
||
if (is_string($routes)) {
|
||
$routes = [$routes];
|
||
}
|
||
|
||
foreach ($routes as $route) {
|
||
if ($exact) {
|
||
// Точное совпадение
|
||
if ($currentRoute === $route) {
|
||
return true;
|
||
}
|
||
} else {
|
||
// Частичное совпадение (начинается с)
|
||
// Исключаем пустую строку, так как она совпадает с любым маршрутом
|
||
if ($route === '') {
|
||
// Для пустого маршрута проверяем только корень
|
||
if ($currentRoute === '') {
|
||
return true;
|
||
}
|
||
} elseif (strpos($currentRoute, $route) === 0) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Получает текущий маршрут без базового URL
|
||
*/
|
||
public function getCurrentRoute(): string
|
||
{
|
||
$uri = service('uri');
|
||
$route = $uri->getRoutePath();
|
||
|
||
// Убираем начальный слеш если есть
|
||
return ltrim($route, '/');
|
||
}
|
||
|
||
/**
|
||
* Генерирует HTML для кнопок действий в строке таблицы
|
||
*
|
||
* @param object|array $item Данные строки (объект или массив)
|
||
* @param array $actions Массив конфигураций действий
|
||
* @return string HTML код кнопок
|
||
*/
|
||
public function renderActions($item, array $actions = []): string
|
||
{
|
||
if (empty($actions)) {
|
||
return '';
|
||
}
|
||
|
||
// Конвертируем объект в массив для доступа к свойствам
|
||
$itemArray = $this->objectToArray($item);
|
||
|
||
// DEBUG: логируем для отладки
|
||
log_message('debug', 'renderActions: item type = ' . gettype($item));
|
||
log_message('debug', 'renderActions: item keys = ' . (is_array($itemArray) ? implode(', ', array_keys($itemArray)) : 'N/A'));
|
||
|
||
$html = '<div class="btn-group btn-group-sm">';
|
||
|
||
foreach ($actions as $action) {
|
||
$label = $action['label'] ?? 'Action';
|
||
$urlPattern = $action['url'] ?? '#';
|
||
$icon = $action['icon'] ?? '';
|
||
$class = $action['class'] ?? 'btn-outline-secondary';
|
||
$title = $action['title'] ?? $label;
|
||
$target = $action['target'] ?? '';
|
||
$type = $action['type'] ?? '';
|
||
|
||
// Проверяем условия для показа кнопки
|
||
// Владелец не может быть изменён, заблокирован или удалён
|
||
if ($itemArray['role'] ?? '' === 'owner') {
|
||
continue;
|
||
}
|
||
|
||
// Блокировка - только для активных пользователей
|
||
if ($type === 'block' && ($itemArray['status'] ?? '') !== 'active') {
|
||
continue;
|
||
}
|
||
|
||
// Разблокировка - только для заблокированных
|
||
if ($type === 'unblock' && ($itemArray['status'] ?? '') !== 'blocked') {
|
||
continue;
|
||
}
|
||
|
||
// Подставляем значения из item в URL
|
||
$url = $this->interpolate($urlPattern, $itemArray);
|
||
|
||
// Формируем HTML кнопки/ссылки
|
||
$iconHtml = $icon ? '<i class="' . esc($icon) . '"></i> ' : '';
|
||
$targetAttr = $target ? ' target="' . esc($target) . '"' : '';
|
||
|
||
$html .= '<a href="' . esc($url) . '" '
|
||
. 'class="btn ' . esc($class) . '" '
|
||
. 'title="' . esc($title) . '"'
|
||
. $targetAttr . '>'
|
||
. $iconHtml . esc($label)
|
||
. '</a>';
|
||
}
|
||
|
||
$html .= '</div>';
|
||
|
||
return $html;
|
||
}
|
||
|
||
/**
|
||
* Конвертирует объект в массив (включая защищённые свойства)
|
||
*/
|
||
private function objectToArray($data): array
|
||
{
|
||
if (is_array($data)) {
|
||
return $data;
|
||
}
|
||
|
||
if (is_object($data)) {
|
||
// Используем json_decode/encode для надёжного извлечения всех свойств
|
||
$json = json_encode($data);
|
||
return json_decode($json, true);
|
||
}
|
||
|
||
return [];
|
||
}
|
||
|
||
/**
|
||
* Рендерит значение ячейки таблицы
|
||
*
|
||
* @param object|array $item Данные строки
|
||
* @param string $key Ключ поля для отображения
|
||
* @param array $config Конфигурация колонки (опционально)
|
||
* @return string HTML код ячейки
|
||
*/
|
||
public function renderCell($item, string $key, array $config = []): string
|
||
{
|
||
$itemArray = $this->objectToArray($item);
|
||
$value = $itemArray[$key] ?? null;
|
||
|
||
// Если значение пустое и есть значение по умолчанию
|
||
if (($value === null || $value === '' || $value === false) && isset($config['default'])) {
|
||
return $config['default'];
|
||
}
|
||
|
||
// Если указан шаблон, используем его для рендеринга
|
||
if (isset($config['template'])) {
|
||
$template = $config['template'];
|
||
// Подставляем все значения из item в шаблон
|
||
foreach ($itemArray as $k => $v) {
|
||
$template = str_replace('{' . $k . '}', esc($v ?? ''), $template);
|
||
}
|
||
return $template;
|
||
}
|
||
|
||
// Применяем тип преобразования если указан
|
||
if (isset($config['type'])) {
|
||
switch ($config['type']) {
|
||
case 'email':
|
||
if ($value) {
|
||
return '<a href="mailto:' . esc($value) . '">' . esc($value) . '</a>';
|
||
}
|
||
return $config['default'] ?? '—';
|
||
|
||
case 'phone':
|
||
if ($value) {
|
||
return '<a href="tel:' . esc($value) . '">' . esc($value) . '</a>';
|
||
}
|
||
return $config['default'] ?? '—';
|
||
|
||
case 'date':
|
||
if ($value) {
|
||
return esc(date('d.m.Y', strtotime($value)));
|
||
}
|
||
return $config['default'] ?? '—';
|
||
|
||
case 'datetime':
|
||
if ($value) {
|
||
return esc(date('d.m.Y H:i', strtotime($value)));
|
||
}
|
||
return $config['default'] ?? '—';
|
||
|
||
case 'boolean':
|
||
case 'bool':
|
||
if ($value) {
|
||
return '<span class="badge bg-success">Да</span>';
|
||
}
|
||
return '<span class="badge bg-secondary">Нет</span>';
|
||
|
||
case 'role_badge':
|
||
return $this->roleBadge((string) $value);
|
||
|
||
case 'status_badge':
|
||
return $this->statusBadge((string) $value);
|
||
|
||
case 'user_display':
|
||
// Отображение пользователя с аватаром и именем/email
|
||
$name = $itemArray['user_name'] ?? '';
|
||
$email = $itemArray['user_email'] ?? $value;
|
||
$avatar = $itemArray['user_avatar'] ?? '';
|
||
if (!empty($avatar)) {
|
||
$avatar = '/uploads/avatars/' . $itemArray['user_avatar'];
|
||
}
|
||
$avatarHtml = $avatar
|
||
? '<img src="' . esc($avatar) . '" alt="" style="width: 32px; height: 32px; object-fit: cover; border-radius: 50%;">'
|
||
: '<div class="bg-light rounded-circle d-flex align-items-center justify-content-center" style="width: 32px; height: 32px;"><i class="fa-solid fa-user text-muted small"></i></div>';
|
||
return '<div class="d-flex align-items-center gap-2">' . $avatarHtml . '<div><div class="fw-medium">' . esc($name ?: $email) . '</div>' . ($name ? '<div class="small text-muted">' . esc($email) . '</div>' : '') . '</div></div>';
|
||
|
||
case 'uppercase':
|
||
return $value ? esc(strtoupper($value)) : ($config['default'] ?? '');
|
||
|
||
case 'lowercase':
|
||
return $value ? esc(strtolower($value)) : ($config['default'] ?? '');
|
||
|
||
case 'truncate':
|
||
$length = $config['length'] ?? 50;
|
||
if ($value && strlen($value) > $length) {
|
||
return esc(substr($value, 0, $length)) . '...';
|
||
}
|
||
return esc($value ?? '');
|
||
|
||
case 'currency':
|
||
if ($value !== null && $value !== '') {
|
||
return number_format((float) $value, 0, '.', ' ') . ' ₽';
|
||
}
|
||
return $config['default'] ?? '—';
|
||
|
||
case 'percent':
|
||
if ($value !== null && $value !== '') {
|
||
return esc((float) $value) . '%';
|
||
}
|
||
return $config['default'] ?? '—';
|
||
}
|
||
}
|
||
|
||
// По умолчанию просто возвращаем значение
|
||
return $value !== null && $value !== '' ? esc((string) $value) : '—';
|
||
}
|
||
|
||
/**
|
||
* Подставляет значения из данных в шаблон строки
|
||
*
|
||
* @param string $pattern Шаблон с плейсхолдерами вида {field_name}
|
||
* @param object|array $data Данные для подстановки
|
||
* @return string Результирующая строка
|
||
*/
|
||
private function interpolate(string $pattern, $data): string
|
||
{
|
||
// Конвертируем объект в массив
|
||
$data = is_object($data) ? $this->objectToArray($data) : $data;
|
||
|
||
// Заменяем все {key} на значения
|
||
return preg_replace_callback('/\{(\w+)\}/', function ($matches) use ($data) {
|
||
$key = $matches[1];
|
||
return isset($data[$key]) ? esc($data[$key]) : $matches[0];
|
||
}, $pattern);
|
||
}
|
||
}
|