bp/app/Libraries/Twig/TwigGlobalsExtension.php

539 lines
20 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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']]),
// System role functions (superadmin)
new TwigFunction('is_superadmin', [$this, 'isSuperadmin'], ['is_safe' => ['html']]),
new TwigFunction('is_system_admin', [$this, 'isSystemAdmin'], ['is_safe' => ['html']]),
new TwigFunction('get_system_role', [$this, 'getSystemRole'], ['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>';
}
// ========================================
// System Role Functions (superadmin)
// ========================================
public function isSuperadmin(): bool
{
return service('access')->isSuperadmin();
}
public function isSystemAdmin(): bool
{
return service('access')->isSystemAdmin();
}
public function getSystemRole(): ?string
{
return service('access')->getSystemRole();
}
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);
}
}