From 76287923e8300c1e56e6b01fee899ee0824b4352 Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Thu, 12 Feb 2026 22:54:47 +0100 Subject: [PATCH] refactor layouts module to domain/di and prepare 0.256 release --- DATABASE_STRUCTURE.md | 40 ++ PROJECT_STRUCTURE.md | 12 +- REFACTORING_PLAN.md | 19 +- TESTING.md | 15 + admin/templates/layouts/layout-edit.php | 12 +- admin/templates/layouts/layouts-list.php | 55 +-- .../templates/layouts/subcategories-list.php | 20 + admin/templates/layouts/subpages-list.php | 8 +- .../Domain/Languages/LanguagesRepository.php | 21 +- autoload/Domain/Layouts/LayoutsRepository.php | 343 ++++++++++++++++++ .../admin/Controllers/ArticlesController.php | 11 +- .../admin/Controllers/LayoutsController.php | 172 +++++++++ autoload/admin/class.Site.php | 14 +- autoload/admin/controls/class.Layouts.php | 43 --- autoload/admin/factory/class.Layouts.php | 244 ++++--------- autoload/admin/view/class.Layouts.php | 21 -- .../Languages/LanguagesRepositoryTest.php | 34 +- .../Domain/Layouts/LayoutsRepositoryTest.php | 110 ++++++ .../Controllers/ArticlesControllerTest.php | 18 +- .../Controllers/LayoutsControllerTest.php | 56 +++ updates/0.20/ver_0.256.zip | Bin 0 -> 22213 bytes updates/0.20/ver_0.256_files.txt | 2 + updates/changelog.php | 12 +- updates/versions.php | 3 +- 24 files changed, 970 insertions(+), 315 deletions(-) create mode 100644 admin/templates/layouts/subcategories-list.php create mode 100644 autoload/Domain/Layouts/LayoutsRepository.php create mode 100644 autoload/admin/Controllers/LayoutsController.php delete mode 100644 autoload/admin/controls/class.Layouts.php delete mode 100644 autoload/admin/view/class.Layouts.php create mode 100644 tests/Unit/Domain/Layouts/LayoutsRepositoryTest.php create mode 100644 tests/Unit/admin/Controllers/LayoutsControllerTest.php create mode 100644 updates/0.20/ver_0.256.zip create mode 100644 updates/0.20/ver_0.256_files.txt diff --git a/DATABASE_STRUCTURE.md b/DATABASE_STRUCTURE.md index 99353fe..323bfc7 100644 --- a/DATABASE_STRUCTURE.md +++ b/DATABASE_STRUCTURE.md @@ -209,3 +209,43 @@ Slownik tlumaczen panelu/frontendu. **Uzywane w:** `Domain\\Languages\\LanguagesRepository`, `admin\\Controllers\\LanguagesController`, `front\\factory\\Languages` **Aktualizacja 2026-02-12:** modul jezykow i tlumaczen (`pp_langs`, `pp_langs_translations`) obslugiwany przez `Domain\\Languages\\LanguagesRepository`. + +## pp_layouts +Szablony layoutow (HTML/CSS/JS + flagi domyslne). + +| Kolumna | Opis | +|---------|------| +| id | PK | +| name | Nazwa szablonu | +| html | Kod HTML | +| css | Kod CSS | +| js | Kod JS | +| m_html | Kod HTML mobilny | +| m_css | Kod CSS mobilny | +| m_js | Kod JS mobilny | +| status | Domyslny layout stron (0/1) | +| categories_default | Domyslny layout kategorii (0/1) | + +**Uzywane w:** `Domain\\Layouts\\LayoutsRepository`, `admin\\Controllers\\LayoutsController`, `front\\factory\\Layouts` + +## pp_layouts_pages +Przypisanie layoutow do stron CMS. + +| Kolumna | Opis | +|---------|------| +| layout_id | FK do pp_layouts | +| page_id | FK do pp_pages | + +**Uzywane w:** `Domain\\Layouts\\LayoutsRepository`, `front\\factory\\Layouts` + +## pp_layouts_categories +Przypisanie layoutow do kategorii sklepu. + +| Kolumna | Opis | +|---------|------| +| layout_id | FK do pp_layouts | +| category_id | FK do pp_shop_categories | + +**Uzywane w:** `Domain\\Layouts\\LayoutsRepository`, `front\\factory\\Layouts` + +**Aktualizacja 2026-02-12 (ver. 0.256):** modul `/admin/layouts` korzysta z `Domain\\Layouts\\LayoutsRepository` (DI kontroler + fasada legacy). diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md index 75d5034..8a54078 100644 --- a/PROJECT_STRUCTURE.md +++ b/PROJECT_STRUCTURE.md @@ -408,4 +408,14 @@ Aktualnie w suite są też testy modułów `Dictionaries`, `Articles` i `Users` - UPDATE: w admin/Site fabryki DI dla Articles, Banners, Settings, Dictionaries przekazuja rowniez LanguagesRepository. - UPDATE: legacy admin/controls/* oraz admin/factory/Shop* przepiete z admin/factory/Languages::languages_list() na bezposrednie wywolania LanguagesRepository. - FIX: autoload/admin/factory/class.Languages.php uzywa pelnego znacznika Menu: - layout['pages'] );?> + $menu['pages'], + 'layout_pages' => $this -> layout['pages'], + 'step' => 1 + ] );?> @@ -202,8 +206,8 @@ ob_start(); layout['categories'] ) and in_array( $category['id'], $this -> layout['categories'] ) ):?>checked="checked" /> dlang]['title'];?> - \admin\factory\ShopCategory::subcategories( $category['id'] ), + $category['subcategories'], 'product_categories' => $this -> layout['categories'], 'dlang' => $this -> dlang ] );?> @@ -250,4 +254,4 @@ $grid -> persist_edit = true; $grid -> id_param = 'id'; echo $grid -> draw(); -?> \ No newline at end of file +?> diff --git a/admin/templates/layouts/layouts-list.php b/admin/templates/layouts/layouts-list.php index 2e7a218..0f89c5b 100644 --- a/admin/templates/layouts/layouts-list.php +++ b/admin/templates/layouts/layouts-list.php @@ -1,54 +1 @@ - gdb_opt = $gdb; -$grid -> order = [ 'column' => 'name', 'type' => 'ASC' ]; -$grid -> search = [ - [ 'name' => 'Nazwa', 'db' => 'name', 'type' => 'text' ], - [ 'name' => 'Szablon domyślny', 'db' => 'status', 'type' => 'select', 'replace' => [ 'array' => [ 0 => 'nie', 1 => 'tak' ] ] ] - ]; -$grid -> columns_view = [ - [ - 'name' => 'Lp.', - 'th' => [ 'class' => 'g-lp' ], - 'td' => [ 'class' => 'g-center' ], - 'autoincrement' => true - ], - [ - 'name' => 'Nazwa', - 'db' => 'name', - 'php' => 'echo "[name]";', - 'sort' => true - ], - [ - 'name' => 'Szablon domyślny', - 'db' => 'status', - 'replace' => [ 'array' => [ 0 => 'nie', 1 => 'tak' ] ], - 'td' => [ 'class' => 'g-center' ], - 'th' => [ 'class' => 'g-center', 'style' => 'width: 150px;' ] - ], - [ - 'name' => 'Szablon domyślny (kategorie)', - 'db' => 'categories_default', - 'replace' => [ 'array' => [ 0 => 'nie', 1 => 'tak' ] ], - 'td' => [ 'class' => 'g-center' ], - 'th' => [ 'class' => 'g-center', 'style' => 'width: 150px;' ] - ], - [ - 'name' => 'Akcja', - 'action' => [ 'type' => 'edit', 'url' => '/admin/layouts/layout_edit/id=[id]' ], - 'th' => [ 'class' => 'g-center' ], - 'td' => [ 'class' => 'g-center', 'style' => 'width: 70px;' ] - ], - [ - 'name' => 'Akcja', - 'action' => [ 'type' => 'delete', 'url' => '/admin/layouts/layout_delete/id=[id]' ], - 'th' => [ 'class' => 'g-center' ], - 'td' => [ 'class' => 'g-center', 'style' => 'width: 70px;' ] - ] - ]; -$grid -> buttons = [ - [ 'label' => 'Dodaj szablon', 'url' => '/admin/layouts/layout_edit/', 'icon' => 'fa-plus-circle', 'class' => 'btn-success' ] - ]; -echo $grid -> draw(); \ No newline at end of file + $this->viewModel]); ?> diff --git a/admin/templates/layouts/subcategories-list.php b/admin/templates/layouts/subcategories-list.php new file mode 100644 index 0000000..e1e5bf5 --- /dev/null +++ b/admin/templates/layouts/subcategories-list.php @@ -0,0 +1,20 @@ + categories ) ):?> +
    + categories as $category ):?> +
  1. +
    + + ';?> + product_categories ) and in_array( $category['id'], $this -> product_categories ) ):?>checked="checked" /> + dlang]['title'];?> +
    + $category['subcategories'], + 'product_categories' => $this -> product_categories, + 'dlang' => $this -> dlang + ] );?> +
  2. + +
+ + diff --git a/admin/templates/layouts/subpages-list.php b/admin/templates/layouts/subpages-list.php index d46b0a9..3f68d21 100644 --- a/admin/templates/layouts/subpages-list.php +++ b/admin/templates/layouts/subpages-list.php @@ -8,9 +8,13 @@ layout_pages, $page['id'], $this -> step + 1 ); + echo \Tpl::view( 'layouts/subpages-list', [ + 'pages' => $page['subpages'], + 'layout_pages' => $this -> layout_pages, + 'step' => $this -> step + 1 + ] ); ?> - \ No newline at end of file + diff --git a/autoload/Domain/Languages/LanguagesRepository.php b/autoload/Domain/Languages/LanguagesRepository.php index 7957acc..cce748d 100644 --- a/autoload/Domain/Languages/LanguagesRepository.php +++ b/autoload/Domain/Languages/LanguagesRepository.php @@ -187,6 +187,26 @@ class LanguagesRepository return is_array($rows) ? $rows : []; } + public function defaultLanguageId(): string + { + $languages = $this->languagesList(); + if (empty($languages)) { + return 'pl'; + } + + foreach ($languages as $language) { + if ((int)($language['start'] ?? 0) === 1 && !empty($language['id'])) { + return (string)$language['id']; + } + } + + if (!empty($languages[0]['id'])) { + return (string)$languages[0]['id']; + } + + return 'pl'; + } + public function deleteLanguage(string $languageId): bool { $languageId = $this->sanitizeLanguageId($languageId); @@ -327,4 +347,3 @@ class LanguagesRepository return ($value === 'on' || $value === 1 || $value === '1' || $value === true) ? 1 : 0; } } - diff --git a/autoload/Domain/Layouts/LayoutsRepository.php b/autoload/Domain/Layouts/LayoutsRepository.php new file mode 100644 index 0000000..7cf6be6 --- /dev/null +++ b/autoload/Domain/Layouts/LayoutsRepository.php @@ -0,0 +1,343 @@ +db = $db; + } + + public function delete(int $layoutId): bool + { + if ((int)$this->db->count('pp_layouts') <= 1) { + return false; + } + + return (bool)$this->db->delete('pp_layouts', ['id' => $layoutId]); + } + + public function find(int $layoutId): array + { + $layout = $this->db->get('pp_layouts', '*', ['id' => $layoutId]); + if (!is_array($layout)) { + return $this->defaultLayout(); + } + + $layout['pages'] = $this->db->select('pp_layouts_pages', 'page_id', ['layout_id' => $layoutId]); + $layout['categories'] = $this->db->select('pp_layouts_categories', 'category_id', ['layout_id' => $layoutId]); + + return $layout; + } + + public function save(array $data): ?int + { + $layoutId = (int)($data['id'] ?? 0); + $status = $this->toSwitchValue($data['status'] ?? 0); + $categoriesDefault = $this->toSwitchValue($data['categories_default'] ?? 0); + + $row = [ + 'name' => (string)($data['name'] ?? ''), + 'html' => (string)($data['html'] ?? ''), + 'css' => (string)($data['css'] ?? ''), + 'js' => (string)($data['js'] ?? ''), + 'm_html' => (string)($data['m_html'] ?? ''), + 'm_css' => (string)($data['m_css'] ?? ''), + 'm_js' => (string)($data['m_js'] ?? ''), + 'status' => $status, + 'categories_default' => $categoriesDefault, + ]; + + if ($status === 1) { + $this->db->update('pp_layouts', ['status' => 0]); + } + + if ($categoriesDefault === 1) { + $this->db->update('pp_layouts', ['categories_default' => 0]); + } + + if ($layoutId <= 0) { + $this->db->insert('pp_layouts', $row); + $layoutId = (int)$this->db->id(); + if ($layoutId <= 0) { + return null; + } + } else { + $this->db->update('pp_layouts', $row, ['id' => $layoutId]); + } + + $this->db->delete('pp_layouts_pages', ['layout_id' => $layoutId]); + $this->syncPages($layoutId, $data['pages'] ?? []); + + $this->db->delete('pp_layouts_categories', ['layout_id' => $layoutId]); + $this->syncCategories($layoutId, $data['categories'] ?? []); + + \S::delete_dir('../temp/'); + + return $layoutId; + } + + public function listAll(): array + { + $rows = $this->db->select('pp_layouts', '*', ['ORDER' => ['name' => 'ASC']]); + return is_array($rows) ? $rows : []; + } + + public function menusWithPages(): array + { + $menus = $this->db->select('pp_menus', '*', ['ORDER' => ['id' => 'ASC']]); + if (!is_array($menus)) { + return []; + } + + foreach ($menus as $key => $menu) { + $menuId = (int)($menu['id'] ?? 0); + $menus[$key]['pages'] = $this->menuPages($menuId, null); + } + + return $menus; + } + + public function categoriesTree($parentId = null): array + { + $rows = $this->db->select('pp_shop_categories', ['id'], [ + 'parent_id' => $parentId, + 'ORDER' => ['o' => 'ASC'], + ]); + + if (!is_array($rows)) { + return []; + } + + $categories = []; + foreach ($rows as $row) { + $categoryId = (int)($row['id'] ?? 0); + if ($categoryId <= 0) { + continue; + } + + $category = $this->db->get('pp_shop_categories', '*', ['id' => $categoryId]); + if (!is_array($category)) { + continue; + } + + $translations = $this->db->select('pp_shop_categories_langs', '*', ['category_id' => $categoryId]); + $category['languages'] = []; + if (is_array($translations)) { + foreach ($translations as $translation) { + $langId = (string)($translation['lang_id'] ?? ''); + if ($langId !== '') { + $category['languages'][$langId] = $translation; + } + } + } + + $category['subcategories'] = $this->categoriesTree($categoryId); + $categories[] = $category; + } + + return $categories; + } + + /** + * @return array{items: array>, total: int} + */ + public function listForAdmin( + array $filters, + string $sortColumn = 'name', + string $sortDir = 'ASC', + int $page = 1, + int $perPage = 15 + ): array { + $allowedSortColumns = [ + 'id' => 'pl.id', + 'name' => 'pl.name', + 'status' => 'pl.status', + 'categories_default' => 'pl.categories_default', + ]; + + $sortSql = $allowedSortColumns[$sortColumn] ?? 'pl.name'; + $sortDir = strtoupper(trim($sortDir)) === 'DESC' ? 'DESC' : 'ASC'; + $page = max(1, $page); + $perPage = min(self::MAX_PER_PAGE, max(1, $perPage)); + $offset = ($page - 1) * $perPage; + + $where = ['1 = 1']; + $params = []; + + $name = trim((string)($filters['name'] ?? '')); + if ($name !== '') { + if (strlen($name) > 255) { + $name = substr($name, 0, 255); + } + $where[] = 'pl.name LIKE :name'; + $params[':name'] = '%' . $name . '%'; + } + + $status = trim((string)($filters['status'] ?? '')); + if ($status === '0' || $status === '1') { + $where[] = 'pl.status = :status'; + $params[':status'] = (int)$status; + } + + $categoriesDefault = trim((string)($filters['categories_default'] ?? '')); + if ($categoriesDefault === '0' || $categoriesDefault === '1') { + $where[] = 'pl.categories_default = :categories_default'; + $params[':categories_default'] = (int)$categoriesDefault; + } + + $whereSql = implode(' AND ', $where); + + $sqlCount = " + SELECT COUNT(0) + FROM pp_layouts AS pl + WHERE {$whereSql} + "; + + $stmtCount = $this->db->query($sqlCount, $params); + $countRows = $stmtCount ? $stmtCount->fetchAll() : []; + $total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0; + + $sql = " + SELECT + pl.id, + pl.name, + pl.status, + pl.categories_default + FROM pp_layouts AS pl + WHERE {$whereSql} + ORDER BY {$sortSql} {$sortDir}, pl.id ASC + LIMIT {$perPage} OFFSET {$offset} + "; + + $stmt = $this->db->query($sql, $params); + $items = $stmt ? $stmt->fetchAll() : []; + + return [ + 'items' => is_array($items) ? $items : [], + 'total' => $total, + ]; + } + + private function syncPages(int $layoutId, $pages): void + { + foreach ($this->normalizeIds($pages) as $pageId) { + $this->db->delete('pp_layouts_pages', ['page_id' => $pageId]); + $this->db->insert('pp_layouts_pages', [ + 'layout_id' => $layoutId, + 'page_id' => $pageId, + ]); + } + } + + private function syncCategories(int $layoutId, $categories): void + { + foreach ($this->normalizeIds($categories) as $categoryId) { + $this->db->delete('pp_layouts_categories', ['category_id' => $categoryId]); + $this->db->insert('pp_layouts_categories', [ + 'layout_id' => $layoutId, + 'category_id' => $categoryId, + ]); + } + } + + /** + * @return int[] + */ + private function normalizeIds($values): array + { + if (!is_array($values)) { + $values = [$values]; + } + + $ids = []; + foreach ($values as $value) { + $id = (int)$value; + if ($id > 0) { + $ids[$id] = $id; + } + } + + return array_values($ids); + } + + private function toSwitchValue($value): int + { + return ($value === 'on' || $value === 1 || $value === '1' || $value === true) ? 1 : 0; + } + + private function defaultLayout(): array + { + return [ + 'id' => 0, + 'name' => '', + 'status' => 0, + 'categories_default' => 0, + 'html' => '', + 'css' => '', + 'js' => '', + 'm_html' => '', + 'm_css' => '', + 'm_js' => '', + 'pages' => [], + 'categories' => [], + ]; + } + + private function menuPages(int $menuId, $parentId = null): array + { + if ($menuId <= 0) { + return []; + } + + $rows = $this->db->select('pp_pages', ['id', 'menu_id', 'status', 'parent_id', 'start'], [ + 'AND' => [ + 'menu_id' => $menuId, + 'parent_id' => $parentId, + ], + 'ORDER' => ['o' => 'ASC'], + ]); + + if (!is_array($rows)) { + return []; + } + + $pages = []; + foreach ($rows as $row) { + $pageId = (int)($row['id'] ?? 0); + if ($pageId <= 0) { + continue; + } + + $row['title'] = $this->pageTitle($pageId); + $row['subpages'] = $this->menuPages($menuId, $pageId); + $pages[] = $row; + } + + return $pages; + } + + private function pageTitle(int $pageId): string + { + $result = $this->db->select('pp_pages_langs', [ + '[><]pp_langs' => ['lang_id' => 'id'], + ], 'title', [ + 'AND' => [ + 'page_id' => $pageId, + 'title[!]' => '', + ], + 'ORDER' => ['o' => 'ASC'], + 'LIMIT' => 1, + ]); + + if (is_array($result) && isset($result[0])) { + return (string)$result[0]; + } + + return ''; + } +} diff --git a/autoload/admin/Controllers/ArticlesController.php b/autoload/admin/Controllers/ArticlesController.php index 807880e..9766a01 100644 --- a/autoload/admin/Controllers/ArticlesController.php +++ b/autoload/admin/Controllers/ArticlesController.php @@ -3,16 +3,23 @@ namespace admin\Controllers; use Domain\Article\ArticleRepository; use Domain\Languages\LanguagesRepository; +use Domain\Layouts\LayoutsRepository; class ArticlesController { private ArticleRepository $repository; private LanguagesRepository $languagesRepository; + private LayoutsRepository $layoutsRepository; - public function __construct(ArticleRepository $repository, LanguagesRepository $languagesRepository) + public function __construct( + ArticleRepository $repository, + LanguagesRepository $languagesRepository, + LayoutsRepository $layoutsRepository + ) { $this->repository = $repository; $this->languagesRepository = $languagesRepository; + $this->layoutsRepository = $layoutsRepository; } /** @@ -189,7 +196,7 @@ class ArticlesController 'article' => $this->repository->find((int)\S::get('id')), 'menus' => \admin\factory\Pages::menus_list(), 'languages' => $this->languagesRepository->languagesList(), - 'layouts' => \admin\factory\Layouts::layouts_list(), + 'layouts' => $this->layoutsRepository->listAll(), 'user' => $user ]); } diff --git a/autoload/admin/Controllers/LayoutsController.php b/autoload/admin/Controllers/LayoutsController.php new file mode 100644 index 0000000..f02c6ac --- /dev/null +++ b/autoload/admin/Controllers/LayoutsController.php @@ -0,0 +1,172 @@ +repository = $repository; + $this->languagesRepository = $languagesRepository; + } + + public function list(): string + { + $sortableColumns = ['name', 'status', 'categories_default']; + + $filterDefinitions = [ + [ + 'key' => 'name', + 'label' => 'Nazwa', + 'type' => 'text', + ], + [ + 'key' => 'status', + 'label' => 'Szablon domyslny', + 'type' => 'select', + 'options' => [ + '' => '- domyslny -', + '1' => 'tak', + '0' => 'nie', + ], + ], + [ + 'key' => 'categories_default', + 'label' => 'Domyslny (kategorie)', + 'type' => 'select', + 'options' => [ + '' => '- kategorie -', + '1' => 'tak', + '0' => 'nie', + ], + ], + ]; + + $listRequest = \admin\Support\TableListRequestFactory::fromRequest( + $filterDefinitions, + $sortableColumns, + 'name' + ); + + $sortDir = $listRequest['sortDir']; + if (trim((string)\S::get('sort')) === '') { + $sortDir = 'ASC'; + } + + $result = $this->repository->listForAdmin( + $listRequest['filters'], + $listRequest['sortColumn'], + $sortDir, + $listRequest['page'], + $listRequest['perPage'] + ); + + $rows = []; + $lp = ($listRequest['page'] - 1) * $listRequest['perPage'] + 1; + foreach ($result['items'] as $item) { + $id = (int)($item['id'] ?? 0); + $name = trim((string)($item['name'] ?? '')); + + $rows[] = [ + 'lp' => $lp++ . '.', + 'name' => '' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '', + 'status' => ((int)($item['status'] ?? 0) === 1) ? 'tak' : 'nie', + 'categories_default' => ((int)($item['categories_default'] ?? 0) === 1) ? 'tak' : 'nie', + '_actions' => [ + [ + 'label' => 'Edytuj', + 'url' => '/admin/layouts/layout_edit/id=' . $id, + 'class' => 'btn btn-xs btn-primary', + ], + [ + 'label' => 'Usun', + 'url' => '/admin/layouts/layout_delete/id=' . $id, + 'class' => 'btn btn-xs btn-danger', + 'confirm' => 'Na pewno chcesz usunac wybrany szablon?', + ], + ], + ]; + } + + $total = (int)$result['total']; + $totalPages = max(1, (int)ceil($total / $listRequest['perPage'])); + + $viewModel = new \admin\ViewModels\Common\PaginatedTableViewModel( + [ + ['key' => 'lp', 'label' => 'Lp.', 'class' => 'text-center', 'sortable' => false], + ['key' => 'name', 'sort_key' => 'name', 'label' => 'Nazwa', 'sortable' => true, 'raw' => true], + ['key' => 'status', 'sort_key' => 'status', 'label' => 'Szablon domyslny', 'class' => 'text-center', 'sortable' => true, 'raw' => true], + ['key' => 'categories_default', 'sort_key' => 'categories_default', 'label' => 'Domyslny (kategorie)', 'class' => 'text-center', 'sortable' => true, 'raw' => true], + ], + $rows, + $listRequest['viewFilters'], + [ + 'column' => $listRequest['sortColumn'], + 'dir' => $sortDir, + ], + [ + 'page' => $listRequest['page'], + 'per_page' => $listRequest['perPage'], + 'total' => $total, + 'total_pages' => $totalPages, + ], + array_merge($listRequest['queryFilters'], [ + 'sort' => $listRequest['sortColumn'], + 'dir' => $sortDir, + 'per_page' => $listRequest['perPage'], + ]), + $listRequest['perPageOptions'], + $sortableColumns, + '/admin/layouts/view_list/', + 'Brak danych w tabeli.', + '/admin/layouts/layout_edit/', + 'Dodaj szablon' + ); + + return \Tpl::view('layouts/layouts-list', [ + 'viewModel' => $viewModel, + ]); + } + + public function edit(): string + { + return \Tpl::view('layouts/layout-edit', [ + 'layout' => $this->repository->find((int)\S::get('id')), + 'menus' => $this->repository->menusWithPages(), + 'categories' => $this->repository->categoriesTree(), + 'dlang' => $this->languagesRepository->defaultLanguageId(), + ]); + } + + public function save(): void + { + $response = ['status' => 'error', 'msg' => 'Podczas zapisywania szablonu wystapil blad. Prosze sprobowac ponownie.']; + $values = json_decode((string)\S::get('values'), true); + + if (is_array($values)) { + $id = $this->repository->save($values); + if (!empty($id)) { + $response = ['status' => 'ok', 'msg' => 'Szablon zostal zapisany.', 'id' => $id]; + } + } + + echo json_encode($response); + exit; + } + + public function delete(): void + { + if ($this->repository->delete((int)\S::get('id'))) { + \S::alert('Szablon zostal usuniety.'); + } + + header('Location: /admin/layouts/view_list/'); + exit; + } + +} diff --git a/autoload/admin/class.Site.php b/autoload/admin/class.Site.php index 5e14bd6..12f12e8 100644 --- a/autoload/admin/class.Site.php +++ b/autoload/admin/class.Site.php @@ -207,7 +207,8 @@ class Site return new \admin\Controllers\ArticlesController( new \Domain\Article\ArticleRepository( $mdb ), - new \Domain\Languages\LanguagesRepository( $mdb ) + new \Domain\Languages\LanguagesRepository( $mdb ), + new \Domain\Layouts\LayoutsRepository( $mdb ) ); }, 'Banners' => function() { @@ -266,6 +267,14 @@ class Site new \Domain\Languages\LanguagesRepository( $mdb ) ); }, + 'Layouts' => function() { + global $mdb; + + return new \admin\Controllers\LayoutsController( + new \Domain\Layouts\LayoutsRepository( $mdb ), + new \Domain\Languages\LanguagesRepository( $mdb ) + ); + }, ]; return self::$newControllers; @@ -309,6 +318,9 @@ class Site 'unit_edit' => 'edit', 'unit_save' => 'save', 'unit_delete' => 'delete', + 'layout_edit' => 'edit', + 'layout_save' => 'save', + 'layout_delete' => 'delete', ]; public static function route() diff --git a/autoload/admin/controls/class.Layouts.php b/autoload/admin/controls/class.Layouts.php deleted file mode 100644 index 9325906..0000000 --- a/autoload/admin/controls/class.Layouts.php +++ /dev/null @@ -1,43 +0,0 @@ - 'error', 'msg' => 'Podczas zapisywania szablonu wystąpił błąd. Proszę spróbować ponownie.' ]; - $values = json_decode( \S::get( 'values' ), true ); - - if ( $id = \admin\factory\Layouts::layout_save( $values['id'], $values['name'], $values['status'], $values['pages'], $values['html'], $values['css'], $values['js'], $values['m_html'], - $values['m_css'], $values['m_js'], $values['categories'], $values['categories_default'] ) - ) - $response = [ 'status' => 'ok', 'msg' => 'Szablon został zapisany.', 'id' => $id ]; - - echo json_encode( $response ); - exit; - } - - public static function layout_edit() - { - return \Tpl::view( 'layouts/layout-edit', [ - 'layout' => \admin\factory\Layouts::layout_details( \S::get( 'id' ) ), - 'menus' => \admin\factory\Layouts::menus_list(), - 'categories' => \admin\factory\ShopCategory::subcategories( null ), - 'dlang' => \front\factory\Languages::default_language() - ] ); - } - - public static function view_list() - { - return \admin\view\Layouts::layouts_list(); - } -} -?> \ No newline at end of file diff --git a/autoload/admin/factory/class.Layouts.php b/autoload/admin/factory/class.Layouts.php index 367932d..4031bb3 100644 --- a/autoload/admin/factory/class.Layouts.php +++ b/autoload/admin/factory/class.Layouts.php @@ -1,190 +1,78 @@ count( 'pp_layouts' ) > 1 ) - return $mdb -> delete( 'pp_layouts', [ 'id' => (int)$layout_id ] ); - return false; - } - - public static function layout_details( $layout_id ) - { - global $mdb; - - $layout = $mdb -> get( 'pp_layouts', '*', [ 'id' => (int)$layout_id ] ); - - $layout['pages'] = $mdb -> select( 'pp_layouts_pages', 'page_id', [ 'layout_id' => (int)$layout_id ] ); - $layout['categories'] = $mdb -> select( 'pp_layouts_categories', 'category_id', [ 'layout_id' => (int)$layout_id ] ); - - return $layout; - } - - public static function layout_save( $layout_id, $name, $status, $pages, $html, $css, $js, $m_html, $m_css, $m_js, $categories, $categories_default ) - { - global $mdb; - - if ( !$layout_id ) + public static function layout_delete($layout_id) { - if ( $status == 'on' ) - $mdb -> update( 'pp_layouts', [ 'status' => 0 ] ); - - if ( $categories_default == 'on' ) - $mdb -> update( 'pp_layouts', [ 'categories_default' => 0 ] ); - - $mdb -> insert( 'pp_layouts', [ - 'name' => $name, - 'html' => $html, - 'css' => $css, - 'js' => $js, - 'm_html' => $m_html, - 'm_css' => $m_css, - 'm_js' => $m_js, - 'status' => $status == 'on' ? 1 : 0, - 'categories_default' => $categories_default == 'on' ? 1 : 0 - ] ); - - $id = $mdb -> id(); - - if ( $id ) - { - if ( is_array( $pages ) ) foreach ( $pages as $page ) - { - $mdb -> delete( 'pp_layouts_pages', [ 'page_id' => (int)$page ] ); - - $mdb -> insert( 'pp_layouts_pages', [ - 'layout_id' => (int)$id, - 'page_id' => (int)$page - ] ); - } - else if ( $pages ) - { - $mdb -> delete( 'pp_layouts_pages', [ 'page_id' => (int)$pages ] ); - - $mdb -> insert( 'pp_layouts_pages', [ - 'layout_id' => (int)$id, - 'page_id' => (int)$pages - ] ); - } - - if ( is_array( $categories ) ) foreach ( $categories as $category ) - { - $mdb -> delete( 'pp_layouts_categories', [ 'category_id' => (int)$category ] ); - - $mdb -> insert( 'pp_layouts_categories', [ - 'layout_id' => (int)$id, - 'category_id' => (int)$category - ] ); - } - else if ( $categories ) - { - $mdb -> delete( 'pp_layouts_categories', [ 'category_id' => (int)$categories ] ); - - $mdb -> insert( 'pp_layouts_categories', [ - 'layout_id' => (int)$id, - 'category_id' => (int)$categories - ] ); - } - - \S::delete_dir( '../temp/' ); - - return $id; - } + return self::repository()->delete((int)$layout_id); } - else + + public static function layout_details($layout_id) { - if ( $status == 'on' ) - $mdb -> update( 'pp_layouts', [ 'status' => 0 ] ); - - if ( $categories_default == 'on' ) - $mdb -> update( 'pp_layouts', [ 'categories_default' => 0 ] ); - - $mdb -> update( 'pp_layouts', [ - 'name' => $name, - 'html' => $html, - 'css' => $css, - 'js' => $js, - 'm_html' => $m_html, - 'm_css' => $m_css, - 'm_js' => $m_js, - 'status' => $status == 'on' ? 1 : 0, - 'categories_default' => $categories_default == 'on' ? 1 : 0 - ], [ - 'id' => $layout_id - ] ); - - $mdb -> delete( 'pp_layouts_pages', [ 'layout_id' => (int)$layout_id ] ); - - if ( is_array( $pages ) ) foreach ( $pages as $page ) - { - $mdb -> delete( 'pp_layouts_pages', [ 'page_id' => (int)$page ] ); - - $mdb -> insert( 'pp_layouts_pages', [ - 'layout_id' => (int)$layout_id, - 'page_id' => (int)$page - ] ); - } - else if ( $pages ) - { - $mdb -> delete( 'pp_layouts_pages', [ 'page_id' => (int)$pages ] ); - - $mdb -> insert( 'pp_layouts_pages', [ - 'layout_id' => (int)$layout_id, - 'page_id' => (int)$pages - ] ); - } - - $mdb -> delete( 'pp_layouts_categories', [ 'layout_id' => (int)$layout_id ] ); - - if ( is_array( $categories ) ) foreach ( $categories as $category ) - { - $mdb -> delete( 'pp_layouts_categories', [ 'category_id' => (int)$category ] ); - - $mdb -> insert( 'pp_layouts_categories', [ - 'layout_id' => (int)$layout_id, - 'category_id' => (int)$category - ] ); - } - else if ( $categories ) - { - $mdb -> delete( 'pp_layouts_categories', [ 'category_id' => (int)$categories ] ); - - $mdb -> insert( 'pp_layouts_categories', [ - 'layout_id' => (int)$layout_id, - 'category_id' => (int)$categories - ] ); - } - - \S::delete_dir( '../temp/' ); - - return $layout_id; + return self::repository()->find((int)$layout_id); } - return false; - } - - public static function menus_list() - { - global $mdb; - - $results = $mdb -> select( 'pp_menus', 'id', [ 'ORDER' => [ 'id' => 'ASC' ] ] ); - if ( is_array( $results ) ) foreach ( $results as $row ) + + public static function layout_save( + $layout_id, + $name, + $status, + $pages, + $html, + $css, + $js, + $m_html, + $m_css, + $m_js, + $categories, + $categories_default + ) { + return self::repository()->save([ + 'id' => $layout_id, + 'name' => $name, + 'status' => $status, + 'pages' => $pages, + 'html' => $html, + 'css' => $css, + 'js' => $js, + 'm_html' => $m_html, + 'm_css' => $m_css, + 'm_js' => $m_js, + 'categories' => $categories, + 'categories_default' => $categories_default, + ]); + } + + public static function menus_list() { - $menu = \admin\factory\Pages::menu_details( $row ); - $menu['pages'] = \admin\factory\Pages::menu_pages( $row ); - - $menus[] = $menu; + $menus = \admin\factory\Pages::menus_list(); + if (!is_array($menus)) { + return []; + } + + foreach ($menus as $key => $menu) { + $menuId = (int)($menu['id'] ?? 0); + if ($menuId <= 0) { + continue; + } + + $menus[$key]['pages'] = \admin\factory\Pages::menu_pages($menuId); + } + + return $menus; + } + + public static function layouts_list() + { + return self::repository()->listAll(); + } + + private static function repository(): LayoutsRepository + { + global $mdb; + return new LayoutsRepository($mdb); } - return $menus; - } - - public static function layouts_list() - { - global $mdb; - return $mdb -> select( 'pp_layouts', '*', [ 'ORDER' => [ 'name' => 'ASC' ] ] ); - } } -?> \ No newline at end of file + diff --git a/autoload/admin/view/class.Layouts.php b/autoload/admin/view/class.Layouts.php deleted file mode 100644 index fd4ea67..0000000 --- a/autoload/admin/view/class.Layouts.php +++ /dev/null @@ -1,21 +0,0 @@ - pages = $pages; - $tpl -> step = $step; - $tpl -> layout_pages = $layout_pages; - return $tpl -> render( 'layouts/subpages-list' ); - } - - public static function layouts_list() - { - $tpl = new \Tpl; - return $tpl -> render( 'layouts/layouts-list' ); - } -} -?> \ No newline at end of file diff --git a/tests/Unit/Domain/Languages/LanguagesRepositoryTest.php b/tests/Unit/Domain/Languages/LanguagesRepositoryTest.php index 36fa2bf..97375f2 100644 --- a/tests/Unit/Domain/Languages/LanguagesRepositoryTest.php +++ b/tests/Unit/Domain/Languages/LanguagesRepositoryTest.php @@ -112,5 +112,37 @@ class LanguagesRepositoryTest extends TestCase $this->assertCount(1, $result['items']); $this->assertSame('pl', $result['items'][0]['id']); } -} + public function testDefaultLanguageIdReturnsLanguageWithStartFlag(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('select') + ->with('pp_langs', '*', ['ORDER' => ['o' => 'ASC']]) + ->willReturn([ + ['id' => 'en', 'start' => 0], + ['id' => 'pl', 'start' => 1], + ]); + + $repository = new LanguagesRepository($mockDb); + $this->assertSame('pl', $repository->defaultLanguageId()); + } + + public function testDefaultLanguageIdFallsBackToFirstLanguageOrPl(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->exactly(2)) + ->method('select') + ->with('pp_langs', '*', ['ORDER' => ['o' => 'ASC']]) + ->willReturnOnConsecutiveCalls( + [ + ['id' => 'en', 'start' => 0], + ], + [] + ); + + $repository = new LanguagesRepository($mockDb); + $this->assertSame('en', $repository->defaultLanguageId()); + $this->assertSame('pl', $repository->defaultLanguageId()); + } +} diff --git a/tests/Unit/Domain/Layouts/LayoutsRepositoryTest.php b/tests/Unit/Domain/Layouts/LayoutsRepositoryTest.php new file mode 100644 index 0000000..fdcb4a8 --- /dev/null +++ b/tests/Unit/Domain/Layouts/LayoutsRepositoryTest.php @@ -0,0 +1,110 @@ +createMock(\medoo::class); + + $mockDb->expects($this->once()) + ->method('get') + ->with('pp_layouts', '*', ['id' => 5]) + ->willReturn(['id' => 5, 'name' => 'Main']); + + $mockDb->expects($this->exactly(2)) + ->method('select') + ->withConsecutive( + ['pp_layouts_pages', 'page_id', ['layout_id' => 5]], + ['pp_layouts_categories', 'category_id', ['layout_id' => 5]] + ) + ->willReturnOnConsecutiveCalls([10, 11], [2, 3]); + + $repository = new LayoutsRepository($mockDb); + $layout = $repository->find(5); + + $this->assertSame(5, $layout['id']); + $this->assertSame([10, 11], $layout['pages']); + $this->assertSame([2, 3], $layout['categories']); + } + + public function testDeleteReturnsFalseWhenOnlyOneLayoutExists(): void + { + $mockDb = $this->createMock(\medoo::class); + + $mockDb->expects($this->once()) + ->method('count') + ->with('pp_layouts') + ->willReturn(1); + + $repository = new LayoutsRepository($mockDb); + $this->assertFalse($repository->delete(1)); + } + + public function testFindReturnsDefaultLayoutWhenRecordDoesNotExist(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with('pp_layouts', '*', ['id' => 999]) + ->willReturn(false); + + $repository = new LayoutsRepository($mockDb); + $layout = $repository->find(999); + + $this->assertSame(0, $layout['id']); + $this->assertSame([], $layout['pages']); + $this->assertSame([], $layout['categories']); + } + + public function testSaveInsertsNewLayoutAndReturnsId(): void + { + $mockDb = $this->createMock(\medoo::class); + + $mockDb->expects($this->once()) + ->method('insert') + ->with('pp_layouts', $this->arrayHasKey('name')); + + $mockDb->expects($this->once()) + ->method('id') + ->willReturn(9); + + $mockDb->expects($this->exactly(2)) + ->method('delete') + ->withConsecutive( + ['pp_layouts_pages', ['layout_id' => 9]], + ['pp_layouts_categories', ['layout_id' => 9]] + ) + ->willReturn(true); + + $repository = new LayoutsRepository($mockDb); + $savedId = $repository->save([ + 'name' => 'Nowy szablon', + 'status' => 0, + 'categories_default' => 0, + ]); + + $this->assertSame(9, $savedId); + } + + public function testListAllReturnsArray(): void + { + $mockDb = $this->createMock(\medoo::class); + + $mockDb->expects($this->once()) + ->method('select') + ->with('pp_layouts', '*', ['ORDER' => ['name' => 'ASC']]) + ->willReturn([ + ['id' => 1, 'name' => 'Default'], + ]); + + $repository = new LayoutsRepository($mockDb); + $rows = $repository->listAll(); + + $this->assertCount(1, $rows); + $this->assertSame('Default', $rows[0]['name']); + } +} diff --git a/tests/Unit/admin/Controllers/ArticlesControllerTest.php b/tests/Unit/admin/Controllers/ArticlesControllerTest.php index 6b1ad26..15b92e2 100644 --- a/tests/Unit/admin/Controllers/ArticlesControllerTest.php +++ b/tests/Unit/admin/Controllers/ArticlesControllerTest.php @@ -5,18 +5,25 @@ use PHPUnit\Framework\TestCase; use admin\Controllers\ArticlesController; use Domain\Article\ArticleRepository; use Domain\Languages\LanguagesRepository; +use Domain\Layouts\LayoutsRepository; class ArticlesControllerTest extends TestCase { private $mockRepository; private $mockLanguagesRepository; + private $mockLayoutsRepository; private $controller; protected function setUp(): void { $this->mockRepository = $this->createMock(ArticleRepository::class); $this->mockLanguagesRepository = $this->createMock(LanguagesRepository::class); - $this->controller = new ArticlesController($this->mockRepository, $this->mockLanguagesRepository); + $this->mockLayoutsRepository = $this->createMock(LayoutsRepository::class); + $this->controller = new ArticlesController( + $this->mockRepository, + $this->mockLanguagesRepository, + $this->mockLayoutsRepository + ); } public function testCanCreateController(): void @@ -26,7 +33,11 @@ class ArticlesControllerTest extends TestCase public function testConstructorAcceptsRepository(): void { - $controller = new ArticlesController($this->mockRepository, $this->mockLanguagesRepository); + $controller = new ArticlesController( + $this->mockRepository, + $this->mockLanguagesRepository, + $this->mockLayoutsRepository + ); $this->assertInstanceOf(ArticlesController::class, $controller); } @@ -69,8 +80,9 @@ class ArticlesControllerTest extends TestCase $constructor = $reflection->getConstructor(); $params = $constructor->getParameters(); - $this->assertCount(2, $params); + $this->assertCount(3, $params); $this->assertEquals('Domain\Article\ArticleRepository', $params[0]->getType()->getName()); $this->assertEquals('Domain\Languages\LanguagesRepository', $params[1]->getType()->getName()); + $this->assertEquals('Domain\Layouts\LayoutsRepository', $params[2]->getType()->getName()); } } diff --git a/tests/Unit/admin/Controllers/LayoutsControllerTest.php b/tests/Unit/admin/Controllers/LayoutsControllerTest.php new file mode 100644 index 0000000..e9d47ed --- /dev/null +++ b/tests/Unit/admin/Controllers/LayoutsControllerTest.php @@ -0,0 +1,56 @@ +mockRepository = $this->createMock(LayoutsRepository::class); + $this->mockLanguagesRepository = $this->createMock(LanguagesRepository::class); + $this->controller = new LayoutsController($this->mockRepository, $this->mockLanguagesRepository); + } + + public function testConstructorAcceptsRepository(): void + { + $controller = new LayoutsController($this->mockRepository, $this->mockLanguagesRepository); + $this->assertInstanceOf(LayoutsController::class, $controller); + } + + public function testHasMainActionMethods(): void + { + $this->assertTrue(method_exists($this->controller, 'list')); + $this->assertTrue(method_exists($this->controller, 'edit')); + $this->assertTrue(method_exists($this->controller, 'save')); + $this->assertTrue(method_exists($this->controller, 'delete')); + } + + public function testActionMethodReturnTypes(): void + { + $reflection = new \ReflectionClass($this->controller); + + $this->assertEquals('string', (string)$reflection->getMethod('list')->getReturnType()); + $this->assertEquals('string', (string)$reflection->getMethod('edit')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('save')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('delete')->getReturnType()); + } + + public function testConstructorRequiresLayoutsRepository(): void + { + $reflection = new \ReflectionClass(LayoutsController::class); + $constructor = $reflection->getConstructor(); + $params = $constructor->getParameters(); + + $this->assertCount(2, $params); + $this->assertEquals('Domain\Layouts\LayoutsRepository', $params[0]->getType()->getName()); + $this->assertEquals('Domain\Languages\LanguagesRepository', $params[1]->getType()->getName()); + } +} diff --git a/updates/0.20/ver_0.256.zip b/updates/0.20/ver_0.256.zip new file mode 100644 index 0000000000000000000000000000000000000000..bb16d608e7c814d4430a49bcad685d566a4c1e76 GIT binary patch literal 22213 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. 0.255 - 12.02.2026
+ver. 0.256 - 12.02.2026
+- NEW - migracja modulu `Layouts` do architektury Domain + DI (`Domain\\Layouts\\LayoutsRepository`, `admin\\Controllers\\LayoutsController`) +- UPDATE - lista `/admin/layouts/view_list/` przepieta z legacy `grid` na `components/table-list` (filtry, sortowanie, paginacja) +- UPDATE - `layouts/layout-edit` korzysta z danych z repozytorium (menus/categories), bez wywolan legacy factory w widoku +- UPDATE - `Domain\\Languages\\LanguagesRepository` rozszerzone o wspolna metode `defaultLanguageId()` +- UPDATE - `admin\\Controllers\\ArticlesController` pobiera layouty przez `Domain\\Layouts\\LayoutsRepository` (DI) +- CLEANUP - usuniete legacy klasy `autoload/admin/controls/class.Layouts.php`, `autoload/admin/view/class.Layouts.php` +
ver. 0.255 - 12.02.2026
- UPDATE - kontrolery admin `Settings`, `Banners`, `Dictionaries`, `Articles` pobieraja liste jezykow przez `Domain\\Languages\\LanguagesRepository` (DI) - UPDATE - routing DI (`admin\\Site`) przekazuje `LanguagesRepository` do kontrolerow `Articles`, `Banners`, `Settings`, `Dictionaries` - UPDATE - aktywne legacy odwolania (`admin\\controls`, `admin\\factory\\Shop*`) przepiete z `admin\\factory\\Languages` na `LanguagesRepository` -- FIX - `autoload/admin/factory/class.Languages.php` uzywa `ver. 0.254 - 12.02.2026
- UPDATE - modul `Languages` w panelu admin przepiety na `Domain\\Languages\\LanguagesRepository` + `admin\\Controllers\\LanguagesController` - UPDATE - migracja widokow languages (`languages-list`, `language-edit`, `translations-list`, `translation-edit`) na `components/table-list` i `components/form-edit` @@ -383,3 +390,4 @@ + diff --git a/updates/versions.php b/updates/versions.php index f6ef0de..b64e3e9 100644 --- a/updates/versions.php +++ b/updates/versions.php @@ -1,5 +1,5 @@