diff --git a/.gitignore b/.gitignore index 4dbb9c8..42349cc 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ vendor views_cache resources/upload resources/tmp -composer.lock \ No newline at end of file +composer.lock +kernel/app_themes \ No newline at end of file diff --git a/bootstrap.php b/bootstrap.php index b9b36fc..5552476 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -7,6 +7,7 @@ $dotenv->load(); include_once __DIR__ . "/bootstrap/db.php"; include_once __DIR__ . "/bootstrap/header.php"; include_once __DIR__ . "/bootstrap/secure.php"; +include_once __DIR__ . "/bootstrap/notification.php"; const ROOT_DIR = __DIR__; const KERNEL_DIR = __DIR__ . "/kernel"; const KERNEL_MODULES_DIR = __DIR__ . "/kernel/modules"; @@ -18,7 +19,7 @@ const KERNEL_APP_MODULES_DIR = KERNEL_DIR . "/app_modules"; const APP_DIR = ROOT_DIR . "/app"; - +\kernel\Theme::$assetsCollector = new \kernel\AssetsCollector(); function getConst($text): array|false|string { diff --git a/bootstrap/notification.php b/bootstrap/notification.php new file mode 100644 index 0000000..ff60dfd --- /dev/null +++ b/bootstrap/notification.php @@ -0,0 +1,4 @@ +addChannel('email', new \kernel\modules\notification\channels\EmailChannel()); \ No newline at end of file diff --git a/kernel/App.php b/kernel/App.php index 7ce9803..e5a34cb 100644 --- a/kernel/App.php +++ b/kernel/App.php @@ -5,6 +5,7 @@ namespace kernel; use kernel\helpers\Debug; +use kernel\modules\notification\NotificationDispatcher; use kernel\modules\user\models\User; use kernel\services\ModuleService; use kernel\services\ThemeService; @@ -21,10 +22,14 @@ class App static User $user; + static NotificationDispatcher $notificationDispatcher; + static array $secure; public ModuleService $moduleService; + static Hook $hook; + public ThemeService $themeService; public static Database $db; @@ -41,6 +46,7 @@ class App public function load(): static { + App::$hook = new Hook(); $this->moduleService = new ModuleService(); App::$collector = new CgRouteCollector(); $this->setRouting(); @@ -53,6 +59,7 @@ class App include KERNEL_DIR . "/routs/admin.php"; include ROOT_DIR . "/rout.php"; $modules_routs = $this->moduleService->getModulesRouts(); + $this->moduleService->setModulesHooks(); foreach ($modules_routs as $rout){ include "$rout"; } diff --git a/kernel/Assets.php b/kernel/Assets.php index 30c2c8b..b0af05a 100644 --- a/kernel/Assets.php +++ b/kernel/Assets.php @@ -7,8 +7,12 @@ class Assets protected array $jsHeader = []; protected array $jsBody = []; + protected array $collectorJs = []; + protected array $css = []; + protected array $collectorCss = []; + protected string $resourceURI = "/resource"; public function __construct(string $resourceURI) @@ -26,7 +30,7 @@ class Assets $this->resourceURI = $resourceURI; } - public function registerJS(string $slug, string $resource, bool $body = true, bool $addResourceURI = true): void + public function registerJS(string $slug, string $resource, bool $body = true, bool $addResourceURI = true, string $after = null): void { $resource = $addResourceURI ? $this->resourceURI . $resource : $resource; if ($body) { @@ -34,12 +38,14 @@ class Assets } else { $this->jsHeader[$slug] = $resource; } + $this->collectorJs[$slug] = ['resource' => $resource, 'after' => $after, 'body' => $body]; } - public function registerCSS(string $slug, string $resource, bool $addResourceURI = true): void + public function registerCSS(string $slug, string $resource, bool $addResourceURI = true, string $after = null): void { $resource = $addResourceURI ? $this->resourceURI . $resource : $resource; $this->css[$slug] = $resource; + $this->collectorCss[$slug] = ['resource' => $resource, 'after' => $after]; } public function getJSAsStr(bool $body = true): void @@ -63,4 +69,14 @@ class Assets } } + public function getCollectorCss(): array + { + return $this->collectorCss; + } + + public function getCollectorJs(): array + { + return $this->collectorJs; + } + } \ No newline at end of file diff --git a/kernel/AssetsCollector.php b/kernel/AssetsCollector.php new file mode 100644 index 0000000..a6d2527 --- /dev/null +++ b/kernel/AssetsCollector.php @@ -0,0 +1,128 @@ +assetsPool[] = $assets; + } + + public function renderCss(): void + { + $css = []; + foreach ($this->assetsPool as $item) { + /** @var Assets $item */ + $css = array_merge($css, $item->getCollectorCss()); + } + + try { + $sortedStyles = $this->sortStyles($css); + + // Выводим отсортированные стили + foreach ($sortedStyles as $style) { + echo '' . "\n"; + } + } catch (RuntimeException $e) { + echo 'Ошибка: ' . $e->getMessage(); + } + } + + public function renderJs(bool $body = true): void + { + $scripts = []; + foreach ($this->assetsPool as $item) { + /** @var Assets $item */ + $scripts = array_merge($scripts, $item->getCollectorJs()); + } + + try { + $sortedScripts = $this->sortScripts($scripts); + + // Разделяем скрипты для head и body + $headScripts = []; + $bodyScripts = []; + + foreach ($sortedScripts as $script) { + if ($script['body']) { + $bodyScripts[] = $script['resource']; + } else { + $headScripts[] = $script['resource']; + } + } + + // Выводим скрипты для head + if ($body){ + $scriptsToRender = $bodyScripts; + } + else { + $scriptsToRender = $headScripts; + } + foreach ($scriptsToRender as $script) { + echo '' . "\n"; + } + } + catch (RuntimeException $e) { + echo 'Ошибка: ' . $e->getMessage(); + } + } + + protected function sortStyles(array $styles): array + { + $sorted = []; + $added = []; + + // Пока не добавим все стили + while (count($sorted) < count($styles)) { + $found = false; + + foreach ($styles as $name => $style) { + // Если стиль еще не добавлен и его зависимости выполнены + if (!isset($added[$name]) && + (empty($style['after']) || isset($added[$style['after']]))) { + $sorted[] = $style['resource']; + $added[$name] = true; + $found = true; + } + } + + if (!$found) { + // Если есть циклическая зависимость + throw new RuntimeException('Обнаружена циклическая зависимость в стилях'); + } + } + + return $sorted; + } + + protected function sortScripts(array $scripts): array + { + $sorted = []; + $added = []; + + while (count($sorted) < count($scripts)) { + $found = false; + + foreach ($scripts as $name => $script) { + if (!isset($added[$name]) && + (empty($script['after']) || isset($added[$script['after']]))) { + $sorted[] = $script; + $added[$name] = true; + $found = true; + } + } + + if (!$found) { + throw new RuntimeException('Обнаружена циклическая зависимость в скриптах'); + } + } + + return $sorted; + } + +} \ No newline at end of file diff --git a/kernel/CollectionTableRenderer.php b/kernel/CollectionTableRenderer.php new file mode 100644 index 0000000..3791fc9 --- /dev/null +++ b/kernel/CollectionTableRenderer.php @@ -0,0 +1,345 @@ + 'filter-row' + ]; + + /** + * Конструктор класса + * + * @param Collection $collection + */ + public function __construct(Collection $collection) + { + $this->collection = $collection; + $this->columns = []; + $this->customColumns = []; + $this->valueProcessors = []; + $this->filters = []; + $this->tableAttributes = [ + 'class' => 'table table-bordered table-striped', + 'id' => 'dataTable' + ]; + } + + /** + * Установка столбцов для отображения + * + * @param array $columns Массив столбцов в формате ['field' => 'Заголовок'] + * @return $this + */ + public function setColumns(array $columns): static + { + $this->columns = $columns; + + return $this; + } + + /** + * Добавление кастомной колонки + * + * @param string $columnName Название колонки (ключ) + * @param string $title Заголовок колонки + * @param callable $callback Функция для генерации содержимого + * @return $this + */ + public function addCustomColumn(string $columnName, string $title, callable $callback): static + { + $this->customColumns[$columnName] = [ + 'title' => $title, + 'callback' => $callback + ]; + + return $this; + } + + /** + * Добавление обработчика значения для колонки + * + * @param string $columnName Название колонки + * @param callable $processor Функция обработки значения + * @return $this + */ + public function addValueProcessor(string $columnName, callable $processor): static + { + $this->valueProcessors[$columnName] = $processor; + + return $this; + } + + /** + * Добавление фильтра для колонки + * + * @param string $columnName Название колонки + * @param string $filterHtml HTML-код фильтра + * @return $this + */ + public function addFilter(string $columnName, string $filterHtml): static + { + $this->filters[$columnName] = $filterHtml; + $this->filterRowEnabled = true; + + return $this; + } + + /** + * Включение/отключение строки фильтров + * + * @param bool $enabled + * @return $this + */ + public function enableFilterRow(bool $enabled = true): static + { + $this->filterRowEnabled = $enabled; + + return $this; + } + + /** + * Установка атрибутов строки фильтров + * + * @param array $attributes + * @return $this + */ + public function setFilterRowAttributes(array $attributes): static + { + $this->filterRowAttributes = array_merge($this->filterRowAttributes, $attributes); + + return $this; + } + + /** + * Установка атрибутов для таблицы + * + * @param array $attributes Массив атрибутов HTML + * @return $this + */ + public function setTableAttributes(array $attributes): static + { + $this->tableAttributes = array_merge($this->tableAttributes, $attributes); + + return $this; + } + + /** + * Получить HTML-код таблицы (без вывода) + * + * @return string + */ + public function getHtml(): string + { + if (empty($this->columns) && empty($this->customColumns) && $this->collection->isNotEmpty()) { + // Автоматически определяем столбцы на основе первой модели + $firstItem = $this->collection->first(); + $this->columns = array_combine( + array_keys($firstItem->toArray()), + array_map('ucfirst', array_keys($firstItem->toArray())) + ); + } + + $html = 'buildAttributes($this->tableAttributes) . '>'; + $html .= $this->renderHeader(); + if ($this->filterRowEnabled) { + $html .= $this->renderFilterRow(); + } + $html .= $this->renderBody(); + $html .= '
'; + + return $html; + } + + + /** + * Генерация HTML-таблицы + * + * @return string + */ + public function render(): void + { + echo $this->getHtml(); + } + + public function fetch(): string + { + return $this->getHtml(); + } + + /** + * Генерация заголовка таблицы + * + * @return string + */ + protected function renderHeader(): string + { + $html = ''; + + // Обычные колонки + foreach ($this->columns as $title) { + $html .= '' . htmlspecialchars($title) . ''; + } + + // Кастомные колонки + foreach ($this->customColumns as $column) { + $html .= '' . htmlspecialchars($column['title']) . ''; + } + + $html .= ''; + + return $html; + } + + /** + * Генерация строки фильтров + * + * @return string + */ + protected function renderFilterRow(): string + { + $html = 'buildAttributes($this->filterRowAttributes) . '>'; + + // Обычные колонки + foreach (array_keys($this->columns) as $field) { + $html .= ''; + if (isset($this->filters[$field])) { + $html .= $this->filters[$field]; + } else { + $html .= ' '; + } + $html .= ''; + } + + // Кастомные колонки + foreach (array_keys($this->customColumns) as $columnName) { + $html .= ''; + if (isset($this->filters[$columnName])) { + $html .= $this->filters[$columnName]; + } else { + $html .= ' '; + } + $html .= ''; + } + + $html .= ''; + + return $html; + } + + /** + * Генерация тела таблицы + * + * @return string + */ + protected function renderBody(): string + { + $html = ''; + + foreach ($this->collection as $item) { + $html .= ''; + + // Обычные колонки + foreach (array_keys($this->columns) as $field) { + $value = $this->getValue($item, $field); + $value = $this->processValue($field, $value, $item); + $html .= '' . $value . ''; + } + + // Кастомные колонки + foreach ($this->customColumns as $columnName => $column) { + $value = call_user_func($column['callback'], $item, $columnName); + $html .= '' . $value . ''; + } + + $html .= ''; + } + + $html .= ''; + + return $html; + } + + /** + * Обработка значения ячейки + * + * @param string $field Название поля + * @param mixed $value Значение + * @param mixed $item Весь элемент коллекции + * @return mixed + */ + protected function processValue(string $field, mixed $value, mixed $item): mixed + { + // Если есть обработчик для этого поля - применяем его + if (isset($this->valueProcessors[$field])) { + $value = call_user_func($this->valueProcessors[$field], $value, $item, $field); + } + + // Если значение не прошло обработку - экранируем его +// if (is_string($value)) { +// return htmlspecialchars($value); +// } + + return $value; + } + + /** + * Получение значения из элемента коллекции + * + * @param mixed $item + * @param string $field + * @return mixed + */ + protected function getValue(mixed $item, string $field): mixed + { + // Поддержка dot-notation для отношений + if (str_contains($field, '.')) { + return data_get($item, $field); + } + + // Поддержка accessor'ов модели + if (method_exists($item, 'get' . ucfirst($field) . 'Attribute')) { + return $item->{$field}; + } + + return $item->{$field} ?? null; + } + + /** + * Сборка HTML-атрибутов + * + * @param array $attributes + * @return string + */ + protected function buildAttributes(array $attributes): string + { + $result = []; + + foreach ($attributes as $key => $value) { + $result[] = $key . '="' . htmlspecialchars($value) . '"'; + } + + return implode(' ', $result); + } + + /** + * Магический метод для преобразования в строку + * + * @return string + */ + public function __toString() + { + return $this->render(); + } +} \ No newline at end of file diff --git a/kernel/EloquentDataProvider.php b/kernel/EloquentDataProvider.php deleted file mode 100644 index d1ca335..0000000 --- a/kernel/EloquentDataProvider.php +++ /dev/null @@ -1,482 +0,0 @@ -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; - } -} \ No newline at end of file diff --git a/kernel/EntityRelation.php b/kernel/EntityRelation.php index 0b29061..6050f20 100644 --- a/kernel/EntityRelation.php +++ b/kernel/EntityRelation.php @@ -209,11 +209,11 @@ class EntityRelation return []; } - public function getAdditionalPropertyByEntityId(string $entity, string $entity_id, string $additionalPropertySlug, array $params = []): string + public function getAdditionalPropertyByEntityId(string $entity, string $entity_id, string $additionalPropertySlug): string { $moduleClass = $this->getAdditionalPropertyClassBySlug($additionalPropertySlug); if ($moduleClass and method_exists($moduleClass, "getItem")) { - return $moduleClass->getItem($entity, $entity_id, $params); + return $moduleClass->getItem($entity, $entity_id); } return ""; @@ -276,20 +276,4 @@ class EntityRelation } } } - - public function callModuleMethod(string $slug, string $method, array $params) - { - $module = $this->moduleService->getModuleInfoBySlug($slug); - if (isset($module['module_class'])) { - $moduleClass = new $module['module_class'](); - if (method_exists($moduleClass, $method)) { - - return call_user_func_array([$moduleClass, $method], $params); - } else { - echo "Метод $method не существует"; - } - } - - return false; - } } \ No newline at end of file diff --git a/kernel/Hook.php b/kernel/Hook.php new file mode 100644 index 0000000..b938356 --- /dev/null +++ b/kernel/Hook.php @@ -0,0 +1,41 @@ +pool[] = [ + 'entity' => $entity, + 'handler' => $handler, + ]; + } + + public function getHooksByEntity(string $entity): array + { + $hooks = []; + foreach ($this->pool as $item){ + if ($item['entity'] === $entity){ + $hooks[] = $item; + } + } + + return $hooks; + } + + public function runHooksByEntity(string $entity, array $params = []): void + { + $response = ''; + $hooks = $this->getHooksByEntity($entity); + foreach ($hooks as $hook){ + $response .= call_user_func_array($hook['handler'], $params ?? []); + } + + echo $response; + } + +} \ No newline at end of file diff --git a/kernel/Request.php b/kernel/Request.php index 257f135..0a6229c 100644 --- a/kernel/Request.php +++ b/kernel/Request.php @@ -132,18 +132,6 @@ class Request return $_GET[$param] ?? $defaultValue; } - /** - * @param string $param - * @return mixed - */ - public function except(string $param): mixed - { - $params = $this->get(); - unset($param); - - return $params; - } - /** * Возвращает POST - параметр. diff --git a/kernel/Theme.php b/kernel/Theme.php new file mode 100644 index 0000000..17ca504 --- /dev/null +++ b/kernel/Theme.php @@ -0,0 +1,15 @@ +registerAsset(new \kernel\admin_themes\default\DefaultAdminThemeAssets($resources)); ?> @@ -18,8 +22,8 @@ $assets = new \kernel\admin_themes\default\DefaultAdminThemeAssets($resources) - getCSSAsSTR(); ?> - getJSAsStr(body: false); ?> + renderCss(); ?> + renderJs(body: false); ?> @@ -63,6 +67,15 @@ $assets = new \kernel\admin_themes\default\DefaultAdminThemeAssets($resources) + + + @@ -83,6 +96,6 @@ $assets = new \kernel\admin_themes\default\DefaultAdminThemeAssets($resources) -getJSAsStr(); ?> +renderJs(); ?> \ No newline at end of file diff --git a/kernel/console/controllers/AdminConsoleController.php b/kernel/console/controllers/AdminConsoleController.php index 4791fec..d119b57 100644 --- a/kernel/console/controllers/AdminConsoleController.php +++ b/kernel/console/controllers/AdminConsoleController.php @@ -49,6 +49,9 @@ class AdminConsoleController extends ConsoleController $out = $this->migrationService->runAtPath("kernel/modules/secure/migrations"); $this->out->r("create secret_code table", "green"); + $out = $this->migrationService->runAtPath("kernel/modules/notification/migrations"); + $this->out->r("create notification table", "green"); + $this->optionService->createFromParams( key: "admin_theme_paths", value: "{\"paths\": [\"{KERNEL_ADMIN_THEMES}\", \"{APP}/admin_themes\"]}", @@ -170,6 +173,13 @@ class AdminConsoleController extends ConsoleController ]); $this->out->r("create item menu option", "green"); + $this->menuService->createItem([ + "label" => "Уведомления", + "url" => "/admin/notification", + "slug" => "notification" + ]); + $this->out->r("create notification option", "green"); + $user = new CreateUserForm(); $user->load([ 'username' => 'admin', diff --git a/kernel/console/controllers/ModuleController.php b/kernel/console/controllers/ModuleController.php index 6267534..2898770 100644 --- a/kernel/console/controllers/ModuleController.php +++ b/kernel/console/controllers/ModuleController.php @@ -119,48 +119,4 @@ class ModuleController extends ConsoleController $this->out->r("Модуль $slug создан", 'green'); } - public function actionConstructController(): void - { - $this->out->r("Введите slug контроллера:", 'yellow'); - $slug = substr(fgets(STDIN), 0, -1); - $slug = strtolower($slug); - - $this->out->r("Введите model контроллера:", 'yellow'); - $model = substr(fgets(STDIN), 0, -1); - - $this->out->r("Введите путь контроллера:", 'yellow'); - $path = substr(fgets(STDIN), 0, -1); - $path = strtolower($path); - - $moduleService = new ModuleService(); - $moduleService->createController([ - 'slug' => $slug, - 'model' => $model, - ], $path); - - $this->out->r("Контроллер $slug создан", 'green'); - } - - public function actionConstructCRUD(): void - { - $this->out->r("Введите slug для CRUD:", 'yellow'); - $slug = substr(fgets(STDIN), 0, -1); - $slug = strtolower($slug); - - $this->out->r("Введите model для CRUD:", 'yellow'); - $model = substr(fgets(STDIN), 0, -1); - - $this->out->r("Введите путь для CRUD:", 'yellow'); - $path = substr(fgets(STDIN), 0, -1); - $path = strtolower($path); - - $moduleService = new ModuleService(); - $moduleService->createCRUD([ - 'slug' => $slug, - 'model' => $model, - ], $path); - - $this->out->r("CRUD $model создан", 'green'); - } - } \ No newline at end of file diff --git a/kernel/console/routs/cli.php b/kernel/console/routs/cli.php index 6395fb6..68f06c7 100644 --- a/kernel/console/routs/cli.php +++ b/kernel/console/routs/cli.php @@ -91,14 +91,6 @@ App::$collector->group(["prefix" => "module"], callback: function (RouteCollecto [\kernel\console\controllers\ModuleController::class, 'actionConstructModule'], additionalInfo: ['description' => 'Сгенерировать модуль'] ); - App::$collector->console('construct/controller', - [\kernel\console\controllers\ModuleController::class, 'actionConstructController'], - additionalInfo: ['description' => 'Сгенерировать контроллер'] - ); - App::$collector->console('construct/crud', - [\kernel\console\controllers\ModuleController::class, 'actionConstructCRUD'], - additionalInfo: ['description' => 'Сгенерировать CRUD'] - ); }); App::$collector->group(["prefix" => "kernel"], callback: function (RouteCollector $router){ diff --git a/kernel/helpers/Files.php b/kernel/helpers/Files.php index aa27972..2360dd7 100644 --- a/kernel/helpers/Files.php +++ b/kernel/helpers/Files.php @@ -125,12 +125,4 @@ class Files } } - public static function isImageByExtension($filename): bool - { - $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']; - $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); - - return in_array($extension, $allowedExtensions); - } - } \ No newline at end of file diff --git a/kernel/helpers/Html.php b/kernel/helpers/Html.php index 369290f..24d44a6 100644 --- a/kernel/helpers/Html.php +++ b/kernel/helpers/Html.php @@ -23,6 +23,12 @@ class Html return ""; } + public static function link(string $title, string $link, array $params = []): string + { + $paramsStr = self::createParams($params); + return "$title"; + } + /** * @param array $data * @return string diff --git a/kernel/helpers/SMTP.php b/kernel/helpers/SMTP.php index 0f6d5e6..8775649 100644 --- a/kernel/helpers/SMTP.php +++ b/kernel/helpers/SMTP.php @@ -24,7 +24,7 @@ class SMTP /** * @throws Exception */ - public function send_html(array $params) + public function send_html(array $params): bool { if (!isset($params['address'])){ return false; @@ -35,6 +35,6 @@ class SMTP $body = $params['body'] ?? 'Нет информации'; $this->mail->msgHTML($body); - $this->mail->send(); + return $this->mail->send(); } } \ No newline at end of file diff --git a/kernel/helpers/Url.php b/kernel/helpers/Url.php deleted file mode 100644 index cb0daf1..0000000 --- a/kernel/helpers/Url.php +++ /dev/null @@ -1,19 +0,0 @@ -channels[$channelName] = $channel; + } + + public function dispatch(NotificationMessage $notification, User $user): void + { + foreach ($notification->via() as $channelName) { + if (isset($this->channels[$channelName])) { + try { + $this->channels[$channelName]->send($notification, $user); + } catch (\Exception $e) { + Flash::setMessage("error", $e->getMessage()); + } + } + } + } +} \ No newline at end of file diff --git a/kernel/modules/notification/NotificationModule.php b/kernel/modules/notification/NotificationModule.php new file mode 100644 index 0000000..1d5f1b2 --- /dev/null +++ b/kernel/modules/notification/NotificationModule.php @@ -0,0 +1,42 @@ +menuService = new MenuService(); + $this->migrationService = new MigrationService(); + } + + /** + * @throws \Exception + */ + public function init(): void + { + $this->migrationService->runAtPath("kernel/modules/notification/migrations"); + + $this->menuService->createItem([ + "label" => "Уведомления", + "url" => "/admin/notification", + "slug" => "notification" + ]); + } + + /** + * @throws \Exception + */ + public function deactivate(): void + { + $this->migrationService->rollbackAtPath("kernel/modules/notification/migrations"); + $this->menuService->removeItemBySlug("notification"); + } +} \ No newline at end of file diff --git a/kernel/modules/notification/channels/DatabaseChannel.php b/kernel/modules/notification/channels/DatabaseChannel.php new file mode 100644 index 0000000..71d5226 --- /dev/null +++ b/kernel/modules/notification/channels/DatabaseChannel.php @@ -0,0 +1,26 @@ +notifications()->create([ + 'message' => $notification->getMessage(), + 'data' => $notification->toArray() + ]); + + return true; + } catch (\Exception $e) { + Flash::setMessage("error", $e->getMessage()); + return false; + } + } +} \ No newline at end of file diff --git a/kernel/modules/notification/channels/EmailChannel.php b/kernel/modules/notification/channels/EmailChannel.php new file mode 100644 index 0000000..3a40b16 --- /dev/null +++ b/kernel/modules/notification/channels/EmailChannel.php @@ -0,0 +1,34 @@ +email) +// ->send(new \App\Mail\NotificationMail( +// $notification->getSubject(), +// $notification->getMessage() +// )); + + return $smtp->send_html([ + 'address' => $user->email, + 'subject' => $notification->getSubject(), + 'body' => $notification->getMessage(), + 'from_name' => $_ENV['MAIL_SMTP_USERNAME'], + ]); + } +} + diff --git a/kernel/modules/notification/channels/SmsChannel.php b/kernel/modules/notification/channels/SmsChannel.php new file mode 100644 index 0000000..1a10e84 --- /dev/null +++ b/kernel/modules/notification/channels/SmsChannel.php @@ -0,0 +1,22 @@ +phone)) { + return false; + } + + // Интеграция с SMS-сервисом + //$smsService = new \App\Services\SmsService(); + //return $smsService->send($user->phone, $notification->getMessage()); + return true; + } +} \ No newline at end of file diff --git a/kernel/modules/notification/channels/TelegramChannel.php b/kernel/modules/notification/channels/TelegramChannel.php new file mode 100644 index 0000000..69ce1fc --- /dev/null +++ b/kernel/modules/notification/channels/TelegramChannel.php @@ -0,0 +1,42 @@ +botToken = $botToken; + } + + public function send(NotificationMessage $notification, User $user): bool + { + if (empty($user->telegram_chat_id)) { + return false; + } + + $httpClient = new \GuzzleHttp\Client(); + + try { + $response = $httpClient->post("https://api.telegram.org/bot{$this->botToken}/sendMessage", [ + 'form_params' => [ + 'chat_id' => $user->telegram_chat_id, + 'text' => $notification->getMessage(), + 'parse_mode' => 'HTML' + ] + ]); + + return $response->getStatusCode() === 200; + } catch (\Exception $e) { + Flash::setMessage("error", $e->getMessage()); + return false; + } + } +} \ No newline at end of file diff --git a/kernel/modules/notification/contracts/NotificationChannelInterface.php b/kernel/modules/notification/contracts/NotificationChannelInterface.php new file mode 100644 index 0000000..f3d0c85 --- /dev/null +++ b/kernel/modules/notification/contracts/NotificationChannelInterface.php @@ -0,0 +1,11 @@ +channels; + } + + public function addChannel(string $channel): void + { + if (!in_array($channel, $this->channels)) { + $this->channels[] = $channel; + } + } + + public function toArray(): array + { + return [ + 'message' => $this->getMessage(), + 'subject' => $this->getSubject(), + ]; + } +} \ No newline at end of file diff --git a/kernel/modules/notification/controllers/NotificationController.php b/kernel/modules/notification/controllers/NotificationController.php new file mode 100644 index 0000000..86f6502 --- /dev/null +++ b/kernel/modules/notification/controllers/NotificationController.php @@ -0,0 +1,106 @@ +cgView->viewPath = KERNEL_MODULES_DIR . '/notification/views/'; + $this->optionService = new NotificationService(); + } + + public function actionCreate(): void + { + $this->cgView->render('form.php'); + } + + #[NoReturn] public function actionAdd(): void + { + $optionForm = new CreateNotificationForm(); + $optionForm->load($_REQUEST); + if ($optionForm->validate()) { + $option = $this->optionService->create($optionForm); + if ($option) { + Flash::setMessage("success", "notification успешно создана."); + $this->redirect('/admin/notification'); + } + } + Flash::setMessage("error", $optionForm->getErrorsStr()); + $this->redirect('/admin/notification/create'); + } + + public function actionIndex($page_number = 1): void + { + $this->cgView->render('index.php', ['page_number' => $page_number]); + } + + /** + * @throws \Exception + */ + public function actionView(int $id): void + { + $option = Notification::find($id); + + if (!$option) { + throw new \Exception('notification not found'); + } + $this->cgView->render("view.php", ['option' => $option]); + } + + /** + * @throws \Exception + */ + public function actionUpdate(int $id): void + { + $model = Notification::find($id); + + if (!$model) { + throw new \Exception('notification not found'); + } + + $this->cgView->render("form.php", ['model' => $model]); + } + + /** + * @throws \Exception + */ + public function actionEdit(int $id): void + { + $option = Notification::find($id); + if (!$option) { + throw new \Exception('Option not found'); + } + $optionForm = new CreateOptionForm(); + $optionForm->load($_REQUEST); + if ($optionForm->validate()) { + $option = $this->optionService->update($optionForm, $option); + if ($option) { + $this->redirect('/admin/notification/view/' . $option->id); + } + } + + $this->redirect('/admin/notification/update/' . $id); + } + + #[NoReturn] public function actionDelete(int $id): void + { + Notification::find($id)->delete(); + Flash::setMessage("success", "notification успешно удалена."); + $this->redirect('/admin/notification'); + } + +} \ No newline at end of file diff --git a/kernel/modules/notification/manifest.json b/kernel/modules/notification/manifest.json new file mode 100644 index 0000000..cd70aaa --- /dev/null +++ b/kernel/modules/notification/manifest.json @@ -0,0 +1,13 @@ +{ + "name": "Notifications", + "version": "0.1", + "author": "ITGuild", + "slug": "notification", + "type": "entity", + "description": "Notifications module", + "module_class": "kernel\\modules\\notification\\NotificationModule", + "module_class_file": "{KERNEL_MODULES}/notification/NotificationModule.php", + "routs": "routs/notification.php", + "migration_path": "migrations", + "dependence": "user,menu" +} \ No newline at end of file diff --git a/kernel/modules/notification/migrations/2025_07_02_160316_create_notification_table.php b/kernel/modules/notification/migrations/2025_07_02_160316_create_notification_table.php new file mode 100644 index 0000000..04e148c --- /dev/null +++ b/kernel/modules/notification/migrations/2025_07_02_160316_create_notification_table.php @@ -0,0 +1,41 @@ +schema->create('notification', function (Blueprint $table) { + $table->id(); + + // Связь с пользователем + $table->unsignedBigInteger('user_id'); + + // Основные данные уведомления + $table->string('type')->index(); // Класс уведомления (например, App\Notifications\OrderCreated) + $table->text('message'); // Текст уведомления + $table->string('subject')->nullable(); // Тема (для email) + $table->json('data')->nullable(); // Дополнительные данные в JSON + + // Статус уведомления + $table->boolean('is_read')->default(false); + $table->integer('status')->default(1); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + \kernel\App::$db->schema->dropIfExists('notification'); + } +}; diff --git a/kernel/modules/notification/models/Notification.php b/kernel/modules/notification/models/Notification.php new file mode 100644 index 0000000..385f022 --- /dev/null +++ b/kernel/modules/notification/models/Notification.php @@ -0,0 +1,71 @@ + 'boolean', + 'data' => 'array' + ]; + + public static function labels(): array + { + return [ + 'user_id' => 'Пользователь', + 'message' => 'Сообщение', + 'subject' => 'Тема', + 'is_read' => 'Прочитано', + 'data' => 'Данные', + 'type' => 'Тип', + 'status' => 'Статус' + ]; + } + + public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo + { + return $this->belongsTo(User::class); + } + + public function markAsRead(): static + { + $this->update(['is_read' => true]); + + return $this; + } + + /** + * @return string[] + */ + public static function getStatus(): array + { + return [ + self::DISABLE_STATUS => "Не активный", + self::TO_SEND_STATUS => "На отправку", + self::SENT_STATUS => "Отправлено", + ]; + } + +} \ No newline at end of file diff --git a/kernel/modules/notification/models/User.php b/kernel/modules/notification/models/User.php new file mode 100644 index 0000000..fcc28ff --- /dev/null +++ b/kernel/modules/notification/models/User.php @@ -0,0 +1,13 @@ +hasMany(Notification::class); + } + +} \ No newline at end of file diff --git a/kernel/modules/notification/models/forms/CreateNotificationForm.php b/kernel/modules/notification/models/forms/CreateNotificationForm.php new file mode 100644 index 0000000..4caa7da --- /dev/null +++ b/kernel/modules/notification/models/forms/CreateNotificationForm.php @@ -0,0 +1,28 @@ + 'required|integer', + 'message' => 'required', + 'is_read' => '', + 'data' => '', + 'type' => 'required', + 'subject' => '', + ]; + } + +} \ No newline at end of file diff --git a/kernel/modules/notification/routs/notification.php b/kernel/modules/notification/routs/notification.php new file mode 100644 index 0000000..7b7a2b7 --- /dev/null +++ b/kernel/modules/notification/routs/notification.php @@ -0,0 +1,19 @@ +group(["prefix" => "admin"], function (RouteCollector $router) { + App::$collector->group(["before" => "auth"], function (RouteCollector $router) { + App::$collector->group(["prefix" => "notification"], callback: function (RouteCollector $router) { + App::$collector->get('/', [\kernel\modules\notification\controllers\NotificationController::class, 'actionIndex']); + App::$collector->get('/page/{page_number}', [\kernel\modules\notification\controllers\NotificationController::class, 'actionIndex']); + App::$collector->get('/create', [\kernel\modules\notification\controllers\NotificationController::class, 'actionCreate']); + App::$collector->post("/", [\kernel\modules\notification\controllers\NotificationController::class, 'actionAdd']); + App::$collector->get('/view/{id}', [\kernel\modules\notification\controllers\NotificationController::class, 'actionView']); + App::$collector->any('/update/{id}', [\kernel\modules\notification\controllers\NotificationController::class, 'actionUpdate']); + App::$collector->any("/edit/{id}", [\kernel\modules\notification\controllers\NotificationController::class, 'actionEdit']); + App::$collector->get('/delete/{id}', [\kernel\modules\notification\controllers\NotificationController::class, 'actionDelete']); + }); + }); +}); \ No newline at end of file diff --git a/kernel/modules/notification/service/NotificationService.php b/kernel/modules/notification/service/NotificationService.php new file mode 100644 index 0000000..93a1cbb --- /dev/null +++ b/kernel/modules/notification/service/NotificationService.php @@ -0,0 +1,57 @@ +user_id = $form_model->getItem('user_id'); + $model->message = $form_model->getItem('message'); + $model->is_read = $form_model->getItem('is_read'); + $model->data = $form_model->getItem('data'); + $model->type = $form_model->getItem('type'); + $model->subject = $form_model->getItem('subject'); + $model->status = $form_model->getItem('status'); + if ($model->save()) { + return $model; + } + + return false; + } + + public function update(FormModel $form_model, Notification $notification): false|Notification + { + $notification->user_id = $form_model->getItem('user_id'); + $notification->message = $form_model->getItem('message'); + $notification->is_read = $form_model->getItem('is_read'); + $notification->data = $form_model->getItem('data'); + $notification->type = $form_model->getItem('type'); + $notification->subject = $form_model->getItem('subject'); + $notification->status = $form_model->getItem('status'); + if ($notification->save()) { + return $notification; + } + + return false; + } + + + +// public function createOptionArr(): array +// { +// foreach (Option::all()->toArray() as $option) { +// $optionArr[$option['id']] = $option['key']; +// } +// if (!empty($optionArr)) { +// return $optionArr; +// } +// return []; +// } + +} \ No newline at end of file diff --git a/kernel/modules/notification/table/columns/OptionDeleteActionColumn.php b/kernel/modules/notification/table/columns/OptionDeleteActionColumn.php new file mode 100644 index 0000000..e7fda5b --- /dev/null +++ b/kernel/modules/notification/table/columns/OptionDeleteActionColumn.php @@ -0,0 +1,16 @@ +baseUrl . $this->prefix . $this->id; + return " Удалить "; + } +} \ No newline at end of file diff --git a/kernel/modules/notification/table/columns/OptionEditActionColumn.php b/kernel/modules/notification/table/columns/OptionEditActionColumn.php new file mode 100644 index 0000000..0ad8bfc --- /dev/null +++ b/kernel/modules/notification/table/columns/OptionEditActionColumn.php @@ -0,0 +1,16 @@ +baseUrl . $this->prefix . $this->id; + return " Редактировать "; + } +} \ No newline at end of file diff --git a/kernel/modules/notification/table/columns/OptionViewActionColumn.php b/kernel/modules/notification/table/columns/OptionViewActionColumn.php new file mode 100644 index 0000000..5c12242 --- /dev/null +++ b/kernel/modules/notification/table/columns/OptionViewActionColumn.php @@ -0,0 +1,18 @@ +baseUrl . $this->prefix . $this->id; + return " Просмотр "; + } + +} \ No newline at end of file diff --git a/kernel/modules/notification/views/form.php b/kernel/modules/notification/views/form.php new file mode 100644 index 0000000..35ca92e --- /dev/null +++ b/kernel/modules/notification/views/form.php @@ -0,0 +1,84 @@ +beginForm(isset($model) ? "/admin/notification/edit/" . $model->id : "/admin/notification"); + +$form->field(class: \itguild\forms\inputs\Select::class, name: "user_id", params: [ + 'class' => "form-control", + 'value' => $model->user_id ?? '' +]) + ->setLabel(Notification::labels()['user_id']) + ->setOptions(\kernel\modules\user\service\UserService::createUsernameArr()) + ->render(); + +$form->field(\itguild\forms\inputs\TextInput::class, 'subject', [ + 'class' => "form-control", + 'placeholder' => Notification::labels()['subject'], + 'value' => $model->subject ?? '' +]) + ->setLabel(Notification::labels()['subject']) + ->render(); + +$form->field(\itguild\forms\inputs\TextInput::class, 'type', [ + 'class' => "form-control", + 'placeholder' => Notification::labels()['type'], + 'value' => $model->type ?? '' +]) + ->setLabel(Notification::labels()['type']) + ->render(); + +$form->field(\itguild\forms\inputs\TextArea::class, 'message', [ + 'class' => "form-control", + 'placeholder' => Notification::labels()['message'], + 'value' => $model->message ?? '' +]) + ->setLabel(Notification::labels()['message']) + ->render(); + +$form->field(\itguild\forms\inputs\Checkbox::class, 'is_read', [ + 'class' => "form-check-input", + 'placeholder' => Notification::labels()['is_read'], + 'value' => $model->is_read ?? '' +]) + ->setLabel(Notification::labels()['is_read']) + ->render(); + +$form->field(\itguild\forms\inputs\Select::class, 'status', [ + 'class' => "form-control", + 'value' => $model->status ?? '' +]) + ->setLabel("Статус") + ->setOptions(Notification::getStatus()) + ->render(); + +?> +
+
+ field(\itguild\forms\inputs\Button::class, name: "btn-submit", params: [ + 'class' => "btn btn-primary ", + 'value' => 'Отправить', + 'typeInput' => 'submit' + ]) + ->render(); + ?> +
+
+ field(\itguild\forms\inputs\Button::class, name: "btn-reset", params: [ + 'class' => "btn btn-warning", + 'value' => 'Сбросить', + 'typeInput' => 'reset' + ]) + ->render(); + ?> +
+
+endForm(); \ No newline at end of file diff --git a/kernel/modules/notification/views/index.php b/kernel/modules/notification/views/index.php new file mode 100644 index 0000000..fe90624 --- /dev/null +++ b/kernel/modules/notification/views/index.php @@ -0,0 +1,44 @@ + $page_number, + 'per_page' => 5, + 'params' => ["class" => "table table-bordered", "border" => "2"], + 'baseUrl' => "/admin/notification", +])); + +$table->beforePrint(function () { + return IconBtnCreateWidget::create(['url' => '/admin/notification/create'])->run(); +}); + +$table->columns([ + "status" => [ + "value" => function ($cell) { + return Notification::getStatus()[$cell]; + }] +]); + +$table->addAction(function($row) { + return IconBtnViewWidget::create(['url' => '/admin/notification/view/' . $row['id']])->run(); +}); +$table->addAction(function($row) { + return IconBtnEditWidget::create(['url' => '/admin/Notification/update/' . $row['id']])->run(); +}); +$table->addAction(function($row) { + return IconBtnDeleteWidget::create(['url' => '/admin/Notification/delete/' . $row['id']])->run(); +}); + +$table->create(); +$table->render(); \ No newline at end of file diff --git a/kernel/modules/notification/views/view.php b/kernel/modules/notification/views/view.php new file mode 100644 index 0000000..63c1ebe --- /dev/null +++ b/kernel/modules/notification/views/view.php @@ -0,0 +1,32 @@ + ["class" => "table table-bordered", "border" => "2"], + 'baseUrl' => "/admin/user", +])); +$table->beforePrint(function () use ($option) { + $btn = IconBtnListWidget::create(['url' => '/admin/option'])->run(); + $btn .= IconBtnEditWidget::create(['url' => '/admin/option/update/' . $option->id])->run(); + $btn .= IconBtnDeleteWidget::create(['url' => '/admin/option/delete/' . $option->id])->run(); + return $btn; +}); + +$table->rows([ + 'status' => (function ($data) { + return \kernel\modules\option\models\Notification::getStatus()[$data]; + }) +]); +$table->create(); +$table->render(); \ No newline at end of file diff --git a/kernel/modules/option/service/OptionService.php b/kernel/modules/option/service/OptionService.php index 15eb5a5..46d2a3e 100644 --- a/kernel/modules/option/service/OptionService.php +++ b/kernel/modules/option/service/OptionService.php @@ -3,6 +3,7 @@ namespace kernel\modules\option\service; use kernel\FormModel; +use kernel\helpers\Debug; use kernel\modules\option\models\Option; class OptionService @@ -49,6 +50,26 @@ class OptionService return false; } + public static function createOrUpdate(string $key, string $value, string $label = ''): false|Option + { + /** @var Option $option */ + $option = self::getItemObject($key); + if (!$option) { + $option = new Option(); + $option->key = $key; + } + $option->value = $value; + if (!empty($label)){ + $option->label = $label; + } + + if ($option->save()) { + return $option; + } + + return false; + } + /** * @param $key * @return false|array|string @@ -63,6 +84,20 @@ class OptionService return false; } + /** + * @param $key + * @return false|array|string|Option + */ + public static function getItemObject($key): false|array|string|Option + { + $item = Option::where("key", $key)->first(); + if ($item){ + return $item; + } + + return false; + } + public static function removeOptionByKey(string $key): bool { $option = Option::where("key", $key)->first(); diff --git a/kernel/modules/post/service/PostService.php b/kernel/modules/post/service/PostService.php index 2294ce2..1afe57d 100644 --- a/kernel/modules/post/service/PostService.php +++ b/kernel/modules/post/service/PostService.php @@ -33,4 +33,9 @@ class PostService return false; } + + public static function getListArr() + { + return Post::pluck('title', 'id')->toArray(); + } } \ No newline at end of file diff --git a/kernel/modules/secure/controllers/SecureController.php b/kernel/modules/secure/controllers/SecureController.php index 8498c77..2211100 100644 --- a/kernel/modules/secure/controllers/SecureController.php +++ b/kernel/modules/secure/controllers/SecureController.php @@ -8,6 +8,7 @@ use kernel\App; use kernel\Flash; use kernel\helpers\Debug; use kernel\Mailing; +use kernel\modules\secure\models\forms\ChangePasswordForm; use kernel\modules\secure\models\forms\LoginEmailForm; use kernel\modules\secure\models\forms\LoginForm; use kernel\modules\secure\models\forms\RegisterForm; @@ -40,7 +41,7 @@ class SecureController extends AdminController // $this->cgView->render('login.php'); } - #[NoReturn] public function actionAuth(): void + #[NoReturn] public function actionAuth($basePath = '/admin'): void { $loginForm = new LoginForm(); $loginForm->load($_REQUEST); @@ -51,19 +52,36 @@ class SecureController extends AdminController else { $field = "username"; } - $user = $this->userService->getByField($field, $loginForm->getItem("username")); if (!$user){ Flash::setMessage("error", "User not found."); - $this->redirect("/admin/login", code: 302); + $this->redirect($basePath . "/login", code: 302); } if (password_verify($loginForm->getItem("password"), $user->password_hash)) { setcookie('user_id', $user->id, time()+60*60*24, '/', $_SERVER['SERVER_NAME'], false); - $this->redirect("/admin", code: 302); + $this->redirect($basePath . '/', code: 302); } else { Flash::setMessage("error", "Username or password incorrect."); - $this->redirect("/admin/login", code: 302); + $this->redirect($basePath . "/login", code: 302); + } + } + + #[NoReturn] public function actionChangePassword($basePath = '/admin'): void + { + $changePasswordForm = new ChangePasswordForm(); + $changePasswordForm->load($_REQUEST); + + $user = UserService::getAuthUser(); + + if (password_verify($changePasswordForm->getItem("old_password"), $user->password_hash)) { + $user->password_hash = password_hash($changePasswordForm->getItem("new_password"), PASSWORD_DEFAULT); + $user->save(); + Flash::setMessage("success", "Пароль успешно изменен."); + $this->redirect($basePath . '', code: 302); + } else { + Flash::setMessage("error", "Username or password incorrect."); + $this->redirect($basePath . "", code: 302); } } @@ -148,25 +166,25 @@ class SecureController extends AdminController $this->cgView->render('register.php'); } - public function actionRegistration(): void + public function actionRegistration($basePath = '/admin'): void { $regForm = new RegisterForm(); $regForm->load($_REQUEST); if ($this->userService->getByField('username', $regForm->getItem("username"))) { Flash::setMessage("error", "Username already exists."); - $this->redirect("/admin/register", code: 302); + $this->redirect($basePath . "/register", code: 302); } if ($this->userService->getByField('email', $regForm->getItem("email"))) { Flash::setMessage("error", "Email already exists."); - $this->redirect("/admin/register", code: 302); + $this->redirect($basePath . "/register", code: 302); } $user = $this->userService->create($regForm); if ($user){ setcookie('user_id', $user->id, time()+60*60*24, '/', $_SERVER['SERVER_NAME'], false); - $this->redirect("/admin", code: 302); + $this->redirect($basePath . "/", code: 302); } } diff --git a/kernel/modules/secure/models/forms/ChangePasswordForm.php b/kernel/modules/secure/models/forms/ChangePasswordForm.php new file mode 100644 index 0000000..5fcb48b --- /dev/null +++ b/kernel/modules/secure/models/forms/ChangePasswordForm.php @@ -0,0 +1,18 @@ + 'required|min-str-len:6|max-str-len:50', + 'new_password' => 'required|min-str-len:6|max-str-len:50', + ]; + } + +} \ No newline at end of file diff --git a/kernel/modules/user/controllers/UserController.php b/kernel/modules/user/controllers/UserController.php index 60a6028..42cdc31 100644 --- a/kernel/modules/user/controllers/UserController.php +++ b/kernel/modules/user/controllers/UserController.php @@ -7,6 +7,7 @@ use JetBrains\PhpStorm\NoReturn; use kernel\AdminController; use kernel\EntityRelation; use kernel\FileUpload; +use kernel\Flash; use kernel\helpers\Debug; use kernel\modules\user\models\forms\CreateUserForm; use kernel\modules\user\models\User; @@ -55,6 +56,7 @@ class UserController extends AdminController $this->redirect("/admin/user/view/" . $user->id); } } + Flash::setMessage("error", $userForm->getErrorsStr()); $this->redirect("/admin/user/create"); } diff --git a/kernel/modules/user/service/UserService.php b/kernel/modules/user/service/UserService.php index ebb26bc..edb7932 100644 --- a/kernel/modules/user/service/UserService.php +++ b/kernel/modules/user/service/UserService.php @@ -2,6 +2,7 @@ namespace kernel\modules\user\service; +use itguild\forms\ActiveForm; use kernel\FormModel; use kernel\helpers\Debug; use kernel\modules\user\models\User; @@ -122,4 +123,12 @@ class UserService $user->save(); } + public static function getList(): array + { + return User::select('id', 'username')->get() + ->pluck('username', 'id') + ->toArray(); + + } + } \ No newline at end of file diff --git a/kernel/modules/user/views/form.php b/kernel/modules/user/views/form.php index 89f94d2..4bb5987 100644 --- a/kernel/modules/user/views/form.php +++ b/kernel/modules/user/views/form.php @@ -47,6 +47,8 @@ if (!isset($model)) { $model = new User(); } $entityRelations->renderEntityAdditionalPropertyFormBySlug("user", $model); + + ?>
diff --git a/kernel/modules/user/views/view.php b/kernel/modules/user/views/view.php index f234627..437221e 100644 --- a/kernel/modules/user/views/view.php +++ b/kernel/modules/user/views/view.php @@ -1,7 +1,7 @@ rows([ } ]); $table->create(); -$table->render(); \ No newline at end of file +$table->render(); + +\kernel\App::$hook->runHooksByEntity('user_view', ['user' => $user]); \ No newline at end of file diff --git a/kernel/services/MigrationService.php b/kernel/services/MigrationService.php index bb0ab9b..d5f436a 100644 --- a/kernel/services/MigrationService.php +++ b/kernel/services/MigrationService.php @@ -53,7 +53,7 @@ class MigrationService $dmr->delete($migrationInstance); } } catch (\Exception $e) { - throw new \Exception('Не удалось откатить миграции: ' . $e->getMessage()); + throw new \Exception('Не удалось откатить миграции'); } } diff --git a/kernel/services/ModuleService.php b/kernel/services/ModuleService.php index 4f54b5a..7fe22a0 100644 --- a/kernel/services/ModuleService.php +++ b/kernel/services/ModuleService.php @@ -278,6 +278,17 @@ class ModuleService return $routs; } + public function setModulesHooks(): void + { + $hooks = []; + $modules = $this->getActiveModules(); + foreach ($modules as $module) { + if (isset($module['hooks'])) { + include $module['path'] . "/" . $module['hooks']; + } + } + } + /** * @return array */ @@ -644,61 +655,4 @@ class ModuleService return true; } - public function createCRUD(array $params, string $modulePath) - { - $slug = $params['slug']; - $model = $params['model']; - - $this->createModuleFileByTemplate( - KERNEL_TEMPLATES_DIR . '/controllers/kernel_controller_template', - $modulePath . '/controllers/' . $model . 'Controller.php', - $params - ); - $this->createModuleFileByTemplate( - KERNEL_TEMPLATES_DIR . '/models/model_template', - $modulePath . '/models/' . $model . '.php', - $params - ); - $this->createModuleFileByTemplate( - KERNEL_TEMPLATES_DIR . '/models/forms/create_form_template', - $modulePath . '/models/forms/Create' . $model . 'Form.php', - $params - ); - $this->createModuleFileByTemplate( - KERNEL_TEMPLATES_DIR . '/services/service_template', - $modulePath . '/services/' . $model . 'Service.php', - $params - ); - - mkdir($modulePath . '/views/' . strtolower($model)); - - $this->createModuleFileByTemplate( - KERNEL_TEMPLATES_DIR . '/views/index_template', - $modulePath . '/views/' . strtolower($model) . '/index.php', - $params - ); - $this->createModuleFileByTemplate( - KERNEL_TEMPLATES_DIR . '/views/view_template', - $modulePath . '/views/' . strtolower($model) . '/view.php', - $params - ); - $this->createModuleFileByTemplate( - KERNEL_TEMPLATES_DIR . '/views/form_template', - $modulePath . '/views/' . strtolower($model) . '/form.php', - $params - ); - } - - public function createController(array $params, string $path): void - { - $slug = $params['slug']; - $model = $params['model']; - - $this->createModuleFileByTemplate( - KERNEL_TEMPLATES_DIR . '/controllers/kernel_controller_template', - $path . '/' . $model . 'Controller.php', - $params - ); - } - } \ No newline at end of file diff --git a/kernel/templates/views/index_template b/kernel/templates/views/index_template index 44953ea..2d54fdf 100644 --- a/kernel/templates/views/index_template +++ b/kernel/templates/views/index_template @@ -59,7 +59,7 @@ $table->columns([ 'value' => $get['title'] ?? '' ] ], -] +]); $table->beforePrint(function () { return IconBtnCreateWidget::create(['url' => '/admin/{slug}/create'])->run();