diff --git a/kernel/EloquentDataProvider.php b/kernel/EloquentDataProvider.php new file mode 100644 index 0000000..d1ca335 --- /dev/null +++ b/kernel/EloquentDataProvider.php @@ -0,0 +1,482 @@ +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 6050f20..0b29061 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): string + public function getAdditionalPropertyByEntityId(string $entity, string $entity_id, string $additionalPropertySlug, array $params = []): string { $moduleClass = $this->getAdditionalPropertyClassBySlug($additionalPropertySlug); if ($moduleClass and method_exists($moduleClass, "getItem")) { - return $moduleClass->getItem($entity, $entity_id); + return $moduleClass->getItem($entity, $entity_id, $params); } return ""; @@ -276,4 +276,20 @@ 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/Request.php b/kernel/Request.php index 0a6229c..257f135 100644 --- a/kernel/Request.php +++ b/kernel/Request.php @@ -132,6 +132,18 @@ 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/admin_themes/default/layout/main.php b/kernel/admin_themes/default/layout/main.php index a015dda..e718104 100644 --- a/kernel/admin_themes/default/layout/main.php +++ b/kernel/admin_themes/default/layout/main.php @@ -63,15 +63,6 @@ $assets = new \kernel\admin_themes\default\DefaultAdminThemeAssets($resources) - - - diff --git a/kernel/console/controllers/ModuleController.php b/kernel/console/controllers/ModuleController.php index 2898770..6267534 100644 --- a/kernel/console/controllers/ModuleController.php +++ b/kernel/console/controllers/ModuleController.php @@ -119,4 +119,48 @@ 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 68f06c7..6395fb6 100644 --- a/kernel/console/routs/cli.php +++ b/kernel/console/routs/cli.php @@ -91,6 +91,14 @@ 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 2360dd7..aa27972 100644 --- a/kernel/helpers/Files.php +++ b/kernel/helpers/Files.php @@ -125,4 +125,12 @@ 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/Url.php b/kernel/helpers/Url.php new file mode 100644 index 0000000..cb0daf1 --- /dev/null +++ b/kernel/helpers/Url.php @@ -0,0 +1,19 @@ +delete($migrationInstance); } } catch (\Exception $e) { - throw new \Exception('Не удалось откатить миграции'); + throw new \Exception('Не удалось откатить миграции: ' . $e->getMessage()); } } diff --git a/kernel/services/ModuleService.php b/kernel/services/ModuleService.php index 42464c3..4f54b5a 100644 --- a/kernel/services/ModuleService.php +++ b/kernel/services/ModuleService.php @@ -644,4 +644,61 @@ 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