diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md index b0132ee..0075910 100644 --- a/PROJECT_STRUCTURE.md +++ b/PROJECT_STRUCTURE.md @@ -246,10 +246,27 @@ tests/ │ └── ProductArchiveControllerTest.php # 6 testów └── Integration/ ``` -**Łącznie: 50 tests, 95 assertions** +**Łącznie: 59 tests, 123 assertions** ## Ostatnie modyfikacje +### 2026-02-06: Migracja Articles::article_delete do DI (ver. 0.245) +- **UPDATE:** `Domain\Article\ArticleRepository` - dodano `archive()` (ustawia status = -1) +- **UPDATE:** `admin\Controllers\ArticlesController` - nowa akcja `delete()` z DI +- **UPDATE:** Router `admin\Site` - dodano `'article_delete' => 'delete'` do `$actionMap` +- **UPDATE:** `admin\factory\Articles::articles_set_archive()` deleguje do `ArticleRepository::archive()` +- **UPDATE:** `admin\controls\Articles::article_delete()` oznaczone `@deprecated` +- Testy: 59 tests, 123 assertions + +### 2026-02-06: Migracja Articles::article_save do DI (ver. 0.244) +- **UPDATE:** `Domain\Article\ArticleRepository` - dodano `save()` + prywatne helpery (`buildArticleRow`, `buildLangRow`, `saveTranslations`, `savePages`, `assignTempFiles`, `assignTempImages`, `deleteMarkedFiles`, `deleteMarkedImages`, `maxPageOrder`) +- **UPDATE:** `admin\Controllers\ArticlesController` - nowa akcja `save()` z DI +- **UPDATE:** Router `admin\Site` - dodano `'article_save' => 'save'` do `$actionMap` +- **UPDATE:** `admin\factory\Articles::article_save()` deleguje do `ArticleRepository::save()` (backward compatibility) +- **UPDATE:** `admin\controls\Articles::article_save()` oznaczone `@deprecated` +- **UPDATE:** `tests/bootstrap.php` - dodano stub `S::seo()` +- Testy: 57 tests, 119 assertions + ### 2026-02-06: Articles cleanup moved to repository (ver. 0.243) - **UPDATE:** `Domain\Article\ArticleRepository` - added `deleteNonassignedImages()` and `deleteNonassignedFiles()` - **UPDATE:** `admin\Controllers\ArticlesController::edit()` uses repository cleanup methods diff --git a/REFACTORING_PLAN.md b/REFACTORING_PLAN.md index 80fdf23..703b72f 100644 --- a/REFACTORING_PLAN.md +++ b/REFACTORING_PLAN.md @@ -181,8 +181,20 @@ grep -r "Product::getQuantity" . - Zmigrowana akcja: `article_edit` -> `edit` (mapowanie w `admin\Site::$actionMap`) - Kompatybilność: `admin\factory\Articles::article_details()` deleguje do nowego repozytorium - Legacy cleanup: metody przejęte przez nowe kontrolery oznaczone `@deprecated` w `admin\controls\Articles|Banners|Settings` - - Testy repozytorium rozszerzone o czyszczenie nieprzypisanych plikw/zdj + - Testy repozytorium rozszerzone o czyszczenie nieprzypisanych plik�w/zdj�� - Aktualizacja: ver. 0.243 + - ✅ ArticleRepository::save() - **ZMIGROWANE** (2026-02-06) 🎉 + - Metoda `save()` z prywatnych helperow (buildArticleRow, buildLangRow, saveTranslations, savePages, assignTempFiles, assignTempImages, deleteMarkedFiles, deleteMarkedImages) + - Zmigrowana akcja: `article_save` -> `save` (mapowanie w `admin\Site::$actionMap`) + - Kompatybilnosc: `admin\factory\Articles::article_save()` deleguje do repozytorium + - Testy: 7 nowych testow save (create, update, translations, pages, marked delete) + - Aktualizacja: ver. 0.244 + - ✅ ArticleRepository::archive() - **ZMIGROWANE** (2026-02-06) 🎉 + - Metoda `archive()` (ustawia status = -1) + - Zmigrowana akcja: `article_delete` -> `delete` (mapowanie w `admin\Site::$actionMap`) + - Kompatybilnosc: `admin\factory\Articles::articles_set_archive()` deleguje do repozytorium + - Testy: 2 nowe testy archive (success, failure) + - Aktualizacja: ver. 0.245 - **Settings** (migracja kontrolera - krok pośredni) - ✅ SettingsRepository - **ZMIGROWANE** (2026-02-05) 🎉 diff --git a/autoload/Domain/Article/ArticleRepository.php b/autoload/Domain/Article/ArticleRepository.php index de36849..4632b8b 100644 --- a/autoload/Domain/Article/ArticleRepository.php +++ b/autoload/Domain/Article/ArticleRepository.php @@ -41,6 +41,294 @@ class ArticleRepository return $article; } + /** + * Zapisuje artykul (tworzy nowy lub aktualizuje istniejacy). + * Zwraca ID artykulu. + */ + public function save(int $articleId, array $data, int $userId): int + { + if (!$articleId) { + return $this->createArticle($data, $userId); + } + + return $this->updateArticle($articleId, $data, $userId); + } + + private function createArticle(array $data, int $userId): int + { + $this->db->insert('pp_articles', $this->buildArticleRow($data, $userId, true)); + + $id = $this->db->id(); + + if (!$id) { + return 0; + } + + $this->saveTranslations($id, $data, true); + $this->savePages($id, $data['pages'] ?? null, true); + $this->assignTempFiles($id); + $this->assignTempImages($id); + + \S::htacces(); + \S::delete_dir('../temp/'); + + return (int)$id; + } + + private function updateArticle(int $articleId, array $data, int $userId): int + { + $this->db->update('pp_articles', $this->buildArticleRow($data, $userId, false), [ + 'id' => $articleId + ]); + + $this->saveTranslations($articleId, $data, false); + $this->savePages($articleId, $data['pages'] ?? null, false); + $this->assignTempFiles($articleId); + $this->assignTempImages($articleId); + $this->deleteMarkedImages($articleId); + $this->deleteMarkedFiles($articleId); + + \S::htacces(); + \S::delete_dir('../temp/'); + + return $articleId; + } + + private function buildArticleRow(array $data, int $userId, bool $isNew): array + { + $row = [ + 'show_title' => ($data['show_title'] ?? '') == 'on' ? 1 : 0, + 'show_date_add' => ($data['show_date_add'] ?? '') == 'on' ? 1 : 0, + 'show_date_modify' => ($data['show_date_modify'] ?? '') == 'on' ? 1 : 0, + 'date_modify' => date('Y-m-d H:i:s'), + 'modify_by' => $userId, + 'layout_id' => !empty($data['layout_id']) ? (int)$data['layout_id'] : null, + 'status' => ($data['status'] ?? '') == 'on' ? 1 : 0, + 'repeat_entry' => ($data['repeat_entry'] ?? '') == 'on' ? 1 : 0, + 'social_icons' => ($data['social_icons'] ?? '') == 'on' ? 1 : 0, + 'show_table_of_contents' => ($data['show_table_of_contents'] ?? '') == 'on' ? 1 : 0, + ]; + + if ($isNew) { + $row['date_add'] = date('Y-m-d H:i:s'); + } + + return $row; + } + + private function buildLangRow($langId, array $data): array + { + return [ + 'lang_id' => $langId, + 'title' => ($data['title'][$langId] ?? '') != '' ? $data['title'][$langId] : null, + 'main_image' => ($data['main_image'][$langId] ?? '') != '' ? $data['main_image'][$langId] : null, + 'entry' => ($data['entry'][$langId] ?? '') != '' ? $data['entry'][$langId] : null, + 'text' => ($data['text'][$langId] ?? '') != '' ? $data['text'][$langId] : null, + 'table_of_contents' => ($data['table_of_contents'][$langId] ?? '') != '' ? $data['table_of_contents'][$langId] : null, + 'meta_title' => ($data['meta_title'][$langId] ?? '') != '' ? $data['meta_title'][$langId] : null, + 'meta_description' => ($data['meta_description'][$langId] ?? '') != '' ? $data['meta_description'][$langId] : null, + 'meta_keywords' => ($data['meta_keywords'][$langId] ?? '') != '' ? $data['meta_keywords'][$langId] : null, + 'seo_link' => \S::seo($data['seo_link'][$langId] ?? '') != '' ? \S::seo($data['seo_link'][$langId]) : null, + 'noindex' => ($data['noindex'][$langId] ?? '') == 'on' ? 1 : 0, + 'copy_from' => ($data['copy_from'][$langId] ?? '') != '' ? $data['copy_from'][$langId] : null, + 'block_direct_access' => ($data['block_direct_access'][$langId] ?? '') == 'on' ? 1 : 0, + ]; + } + + private function saveTranslations(int $articleId, array $data, bool $isNew): void + { + $titles = $data['title'] ?? []; + + foreach ($titles as $langId => $val) { + $langRow = $this->buildLangRow($langId, $data); + + if ($isNew) { + $langRow['article_id'] = $articleId; + $this->db->insert('pp_articles_langs', $langRow); + } else { + $translationId = $this->db->get('pp_articles_langs', 'id', [ + 'AND' => ['article_id' => $articleId, 'lang_id' => $langId] + ]); + + if ($translationId) { + $this->db->update('pp_articles_langs', $langRow, ['id' => $translationId]); + } else { + $langRow['article_id'] = $articleId; + $this->db->insert('pp_articles_langs', $langRow); + } + } + } + } + + private function savePages(int $articleId, $pages, bool $isNew): void + { + if (!$isNew) { + $notIn = [0]; + + if (is_array($pages)) { + foreach ($pages as $page) { + $notIn[] = $page; + } + } elseif ($pages) { + $notIn[] = $pages; + } + + $this->db->delete('pp_articles_pages', [ + 'AND' => ['article_id' => $articleId, 'page_id[!]' => $notIn] + ]); + + $existingPages = $this->db->select('pp_articles_pages', 'page_id', ['article_id' => $articleId]); + + if (!is_array($pages)) { + $pages = [$pages]; + } + + $pages = array_diff($pages, is_array($existingPages) ? $existingPages : []); + } else { + if (!is_array($pages)) { + $pages = $pages ? [$pages] : []; + } + } + + if (is_array($pages)) { + foreach ($pages as $page) { + $order = $this->maxPageOrder() + 1; + + $this->db->insert('pp_articles_pages', [ + 'article_id' => $articleId, + 'page_id' => (int)$page, + 'o' => $order, + ]); + } + } + } + + private function assignTempFiles(int $articleId): void + { + $results = $this->db->select('pp_articles_files', '*', ['article_id' => null]); + + if (!is_array($results)) { + return; + } + + $created = false; + $dir = '/upload/article_files/article_' . $articleId; + + foreach ($results as $row) { + $newFileName = str_replace('/upload/article_files/tmp', $dir, $row['src']); + + if (file_exists('..' . $row['src'])) { + if (!is_dir('../' . $dir) && $created !== true) { + if (mkdir('../' . $dir, 0755, true)) { + $created = true; + } + } + rename('..' . $row['src'], '..' . $newFileName); + } + + $this->db->update('pp_articles_files', [ + 'src' => $newFileName, + 'article_id' => $articleId, + ], ['id' => $row['id']]); + } + } + + private function assignTempImages(int $articleId): void + { + $results = $this->db->select('pp_articles_images', '*', ['article_id' => null]); + + if (!is_array($results)) { + return; + } + + $created = false; + $dir = '/upload/article_images/article_' . $articleId; + + foreach ($results as $row) { + $newFileName = str_replace('/upload/article_images/tmp', $dir, $row['src']); + + if (file_exists('../' . $newFileName)) { + $ext = strrpos($newFileName, '.'); + $fileNameA = substr($newFileName, 0, $ext); + $fileNameB = substr($newFileName, $ext); + + $count = 1; + while (file_exists('../' . $fileNameA . '_' . $count . $fileNameB)) { + $count++; + } + + $newFileName = $fileNameA . '_' . $count . $fileNameB; + } + + if (file_exists('..' . $row['src'])) { + if (!is_dir('../' . $dir) && $created !== true) { + if (mkdir('../' . $dir, 0755, true)) { + $created = true; + } + } + rename('..' . $row['src'], '..' . $newFileName); + } + + $this->db->update('pp_articles_images', [ + 'src' => $newFileName, + 'article_id' => $articleId, + ], ['id' => $row['id']]); + } + } + + private function deleteMarkedImages(int $articleId): void + { + $results = $this->db->select('pp_articles_images', '*', [ + 'AND' => ['article_id' => $articleId, 'to_delete' => 1] + ]); + + if (is_array($results)) { + foreach ($results as $row) { + if (file_exists('../' . $row['src'])) { + unlink('../' . $row['src']); + } + } + } + + $this->db->delete('pp_articles_images', [ + 'AND' => ['article_id' => $articleId, 'to_delete' => 1] + ]); + } + + private function deleteMarkedFiles(int $articleId): void + { + $results = $this->db->select('pp_articles_files', '*', [ + 'AND' => ['article_id' => $articleId, 'to_delete' => 1] + ]); + + if (is_array($results)) { + foreach ($results as $row) { + if (file_exists('../' . $row['src'])) { + unlink('../' . $row['src']); + } + } + } + + $this->db->delete('pp_articles_files', [ + 'AND' => ['article_id' => $articleId, 'to_delete' => 1] + ]); + } + + private function maxPageOrder(): int + { + $max = $this->db->max('pp_articles_pages', 'o'); + return $max ? (int)$max : 0; + } + + /** + * Archiwizuje artykul (ustawia status = -1). + */ + public function archive(int $articleId): bool + { + $result = $this->db->update('pp_articles', ['status' => -1], ['id' => $articleId]); + return (bool)$result; + } + /** * Usuwa nieprzypisane pliki artykulow (article_id = null) wraz z plikami z dysku. */ diff --git a/autoload/admin/Controllers/ArticlesController.php b/autoload/admin/Controllers/ArticlesController.php index affee8a..742e062 100644 --- a/autoload/admin/Controllers/ArticlesController.php +++ b/autoload/admin/Controllers/ArticlesController.php @@ -20,6 +20,37 @@ class ArticlesController return \admin\view\Articles::articles_list(); } + /** + * Zapis artykulu (AJAX) + */ + public function save(): void + { + global $user; + + $values = json_decode(\S::get('values'), true); + $response = ['status' => 'error', 'msg' => 'Podczas zapisywania artykułu wystąpił błąd. Proszę spróbować ponownie.']; + + if ($id = $this->repository->save((int)($values['id'] ?? 0), $values, (int)$user['id'])) { + $response = ['status' => 'ok', 'msg' => 'Artykuł został zapisany.', 'id' => $id]; + } + + echo json_encode($response); + exit; + } + + /** + * Archiwizacja artykulu (ustawia status = -1) + */ + public function delete(): void + { + if ($this->repository->archive((int)\S::get('id'))) { + \S::alert('Artykuł został przeniesiony do archiwum.'); + } + + header('Location: /admin/articles/view_list/'); + exit; + } + /** * Edycja artykulu */ diff --git a/autoload/admin/class.Site.php b/autoload/admin/class.Site.php index 1c2b48a..c1538eb 100644 --- a/autoload/admin/class.Site.php +++ b/autoload/admin/class.Site.php @@ -255,6 +255,8 @@ class Site private static $actionMap = [ 'view_list' => 'list', 'article_edit' => 'edit', + 'article_save' => 'save', + 'article_delete' => 'delete', 'banner_edit' => 'edit', 'banner_save' => 'save', 'banner_delete' => 'delete', diff --git a/autoload/admin/controls/class.Articles.php b/autoload/admin/controls/class.Articles.php index b2fddb7..d4c61cd 100644 --- a/autoload/admin/controls/class.Articles.php +++ b/autoload/admin/controls/class.Articles.php @@ -16,6 +16,10 @@ class Articles return \admin\view\Articles::browse_list(); } + /** + * @deprecated Routing kieruje do admin\Controllers\ArticlesController::delete(). + * Ta metoda pozostaje tylko jako fallback dla starej architektury. + */ public static function article_delete() { if ( \admin\factory\Articles::articles_set_archive( \S::get( 'id' ) ) ) @@ -24,6 +28,10 @@ class Articles exit; } + /** + * @deprecated Routing kieruje do admin\Controllers\ArticlesController::save(). + * Ta metoda pozostaje tylko jako fallback dla starej architektury. + */ public static function article_save() { $response = [ 'status' => 'error', 'msg' => 'Podczas zapisywania artykułu wystąpił błąd. Proszę spróbować ponownie.' ]; diff --git a/autoload/admin/factory/class.Articles.php b/autoload/admin/factory/class.Articles.php index 605c53b..27bf189 100644 --- a/autoload/admin/factory/class.Articles.php +++ b/autoload/admin/factory/class.Articles.php @@ -93,10 +93,14 @@ class Articles return $results[0]['title']; } + /** + * @deprecated Logika przeniesiona do Domain\Article\ArticleRepository::archive(). + */ public static function articles_set_archive( $article_id ) { global $mdb; - return $mdb -> update( 'pp_articles', [ 'status' => -1 ], [ 'id' => (int)$article_id ] ); + $repository = new \Domain\Article\ArticleRepository( $mdb ); + return $repository->archive( (int)$article_id ); } public static function file_name_change( $file_id, $file_name ) @@ -133,301 +137,31 @@ class Articles return $mdb -> max( 'pp_articles_pages', 'o' ); } + /** + * @deprecated Logika przeniesiona do Domain\Article\ArticleRepository::save(). + * Ta metoda pozostaje jako fasada dla backward compatibility. + */ public static function article_save( $article_id, $title, $main_image, $entry, $text, $table_of_contents, $status, $show_title, $show_table_of_contents, $show_date_add, $date_add, $show_date_modify, $date_modify, $seo_link, $meta_title, $meta_description, $meta_keywords, $layout_id, $pages, $noindex, $repeat_entry, $copy_from, $social_icons, $block_direct_access ) { global $mdb, $user; - if ( !$article_id ) - { - $mdb -> insert( 'pp_articles', [ - 'show_title' => $show_title == 'on' ? 1 : 0, - 'show_date_add' => $show_date_add == 'on' ? 1 : 0, - 'show_date_modify' => $show_date_modify == 'on' ? 1 : 0, - 'date_add' => date( 'Y-m-d H:i:s' ), - 'date_modify' => date( 'Y-m-d H:i:s' ), - 'modify_by' => $user['id'], - 'layout_id' => $layout_id ? (int)$layout_id : null, - 'status' => $status == 'on' ? 1 : 0, - 'repeat_entry' => $repeat_entry == 'on' ? 1 : 0, - 'social_icons' => $social_icons == 'on' ? 1 : 0, - 'show_table_of_contents' => $show_table_of_contents == 'on' ? 1 : 0, - ] ); + $repository = new \Domain\Article\ArticleRepository( $mdb ); - $id = $mdb -> id(); - - if ( $id ) - { - foreach ( $title as $key => $val ) - { - $mdb -> insert( 'pp_articles_langs', [ - 'article_id' => (int)$id, - 'lang_id' => $key, - 'title' => $title[$key] != '' ? $title[$key] : null, - 'main_image' => $main_image[$key] != '' ? $main_image[$key] : null, - 'entry' => $entry[$key] != '' ? $entry[$key] : null, - 'text' => $text[$key] != '' ? $text[$key] : null, - 'table_of_contents' => $table_of_contents[$key] != '' ? $table_of_contents[$key] : null, - 'meta_title' => $meta_title[$key] != '' ? $meta_title[$key] : null, - 'meta_description' => $meta_description[$key] != '' ? $meta_description[$key] : null, - 'meta_keywords' => $meta_keywords[$key] != '' ? $meta_keywords[$key] : null, - 'seo_link' => \S::seo( $seo_link[$key] ) != '' ? \S::seo( $seo_link[$key] ) : null, - 'noindex' => $noindex[$key] == 'on' ? 1 : 0, - 'copy_from' => $copy_from[$key] != '' ? $copy_from[$key] : null, - 'block_direct_access' => $block_direct_access[$key] == 'on' ? 1 : 0 - ] ); - } - - if ( is_array( $pages ) ) foreach ( $pages as $page ) - { - $order = self::max_order() + 1; - - $mdb -> insert( 'pp_articles_pages', [ - 'article_id' => (int)$id, - 'page_id' => (int)$page, - 'o' => (int)$order - ] ); - } - else if ( $pages ) - { - $order = self::max_order() + 1; - - $mdb -> insert( 'pp_articles_pages', [ - 'article_id' => (int)$id, - 'page_id' => (int)$pages, - 'o' => (int)$order - ] ); - } - - $results = $mdb -> select( 'pp_articles_files', '*', [ 'article_id' => null ] ); - if ( is_array( $results ) ) foreach ( $results as $row ) - { - $dir = '/upload/article_files/article_' . $id; - - $new_file_name = str_replace( '/upload/article_files/tmp', $dir, $row['src'] ); - - if ( file_exists( '..' . $row['src'] ) ) - { - if ( !is_dir( '../' . $dir ) and $created !== true ) - { - if ( mkdir( '../' . $dir, 0755, true ) ) - $created = true; - } - rename( '..' . $row['src'], '..' . $new_file_name ); - } - - $mdb -> update( 'pp_articles_files', [ 'src' => $new_file_name, 'article_id' => $id ], [ 'id' => $row['id'] ] ); - } - - $created = false; - - /* zdjęcia */ - $results = $mdb -> select( 'pp_articles_images', '*', [ 'article_id' => null ] ); - if ( is_array( $results ) ) foreach ( $results as $row ) - { - $dir = '/upload/article_images/article_' . $id; - - $new_file_name = str_replace( '/upload/article_images/tmp', $dir, $row['src'] ); - - if ( file_exists( '../' . $new_file_name ) ) - { - $ext = strrpos( $new_file_name, '.' ); - $fileName_a = substr( $new_file_name, 0, $ext ); - $fileName_b = substr( $new_file_name, $ext ); - - $count = 1; - - while ( file_exists( '../' . $fileName_a . '_' . $count . $fileName_b ) ) - $count++; - - $new_file_name = $fileName_a . '_' . $count . $fileName_b; - } - - if ( file_exists( '..' . $row['src'] ) ) - { - if ( !is_dir( '../' . $dir ) and $created !== true ) - { - if ( mkdir( '../' . $dir, 0755, true ) ) - $created = true; - } - rename( '..' . $row['src'], '..' . $new_file_name ); - } - - $mdb -> update( 'pp_articles_images', [ 'src' => $new_file_name, 'article_id' => (int)$id ], [ 'id' => $row['id'] ] ); - } - - \S::htacces(); - - \S::delete_dir( '../temp/' ); - - return $id; - } - } - else - { - $mdb -> update( 'pp_articles', [ - 'show_title' => $show_title == 'on' ? 1 : 0, - 'show_date_add' => $show_date_add == 'on' ? 1 : 0, - 'show_date_modify' => $show_date_modify == 'on' ? 1 : 0, - 'date_modify' => date( 'Y-m-d H:i:s' ), - 'modify_by' => $user['id'], - 'layout_id' => $layout_id ? (int)$layout_id : null, - 'status' => $status == 'on' ? 1 : 0, - 'repeat_entry' => $repeat_entry == 'on' ? 1 : 0, - 'social_icons' => $social_icons == 'on' ? 1 : 0, - 'show_table_of_contents' => $show_table_of_contents == 'on' ? 1 : 0, - ], [ - 'id' => (int)$article_id - ] ); - - foreach ( $title as $key => $val ) - { - if ( $translation_id = $mdb -> get( 'pp_articles_langs', 'id', [ 'AND' => [ 'article_id' => $article_id, 'lang_id' => $key ] ] ) ) - $mdb -> update( 'pp_articles_langs', [ - 'lang_id' => $key, - 'title' => $title[$key] != '' ? $title[$key] : null, - 'main_image' => $main_image[$key] != '' ? $main_image[$key] : null, - 'entry' => $entry[$key] != '' ? $entry[$key] : null, - 'text' => $text[$key] != '' ? $text[$key] : null, - 'table_of_contents' => $table_of_contents[$key] != '' ? $table_of_contents[$key] : null, - 'meta_title' => $meta_title[$key] != '' ? $meta_title[$key] : null, - 'meta_description' => $meta_description[$key] != '' ? $meta_description[$key] : null, - 'meta_keywords' => $meta_keywords[$key] != '' ? $meta_keywords[$key] : null, - 'seo_link' => \S::seo( $seo_link[$key] ) != '' ? \S::seo( $seo_link[$key] ) : null, - 'noindex' => $noindex[$key] == 'on' ? 1 : 0, - 'copy_from' => $copy_from[$key] != '' ? $copy_from[$key] : null, - 'block_direct_access' => $block_direct_access[$key] == 'on' ? 1 : 0 - ], [ - 'id' => $translation_id - ] ); - else - $mdb -> insert( 'pp_articles_langs', [ - 'article_id' => (int)$article_id, - 'lang_id' => $key, - 'title' => $title[$key] != '' ? $title[$key] : null, - 'main_image' => $main_image[$key] != '' ? $main_image[$key] : null, - 'entry' => $entry[$key] != '' ? $entry[$key] : null, - 'text' => $text[$key] != '' ? $text[$key] : null, - 'table_of_contents' => $table_of_contents[$key] != '' ? $table_of_contents[$key] : null, - 'meta_title' => $meta_title[$key] != '' ? $meta_title[$key] : null, - 'meta_description' => $meta_description[$key] != '' ? $meta_description[$key] : null, - 'meta_keywords' => $meta_keywords[$key] != '' ? $meta_keywords[$key] : null, - 'seo_link' => \S::seo( $seo_link[$key] ) != '' ? \S::seo( $seo_link[$key] ) : null, - 'noindex' => $noindex[$key] == 'on' ? 1 : 0, - 'copy_from' => $copy_from[$key] != '' ? $copy_from[$key] : null, - 'block_direct_access' => $block_direct_access[$key] == 'on' ? 1 : 0 - ] ); - } - - $not_in = [ 0 ]; - - if ( is_array( $pages ) ) foreach ( $pages as $page ) - $not_in[] = $page; - else if ( $pages ) - $not_in[] = $pages; - - $mdb -> delete( 'pp_articles_pages', [ 'AND' => [ 'article_id' => (int)$article_id, 'page_id[!]' => $not_in ] ] ); - - $pages_tmp = $mdb -> select( 'pp_articles_pages', 'page_id', [ 'article_id' => (int)$article_id ] ); - - if ( !is_array( $pages ) ) - $pages = [ $pages ]; - - $pages = array_diff( $pages, $pages_tmp ); - - if ( is_array( $pages ) ) foreach ( $pages as $page ) - { - $order = self::max_order() + 1; - - $mdb -> insert( 'pp_articles_pages', [ - 'article_id' => (int)$article_id, - 'page_id' => (int)$page, - 'o' => (int)$order - ] ); - } - - $results = $mdb -> select( 'pp_articles_files', '*', [ 'article_id' => null ] ); - if ( is_array( $results ) ) foreach ( $results as $row ) - { - $dir = '/upload/article_files/article_' . $article_id; - - $new_file_name = str_replace( '/upload/article_files/tmp', $dir, $row['src'] ); - - if ( file_exists( '..' . $row['src'] ) ) - { - if ( !is_dir( '../' . $dir ) and $created !== true ) - { - if ( mkdir( '../' . $dir, 0755, true ) ) - $created = true; - } - rename( '..' . $row['src'], '..' . $new_file_name ); - } - - $mdb -> update( 'pp_articles_files', [ 'src' => $new_file_name, 'article_id' => (int)$article_id ], [ 'id' => $row['id'] ] ); - } - - $created = false; - - /* zdjęcia */ - $results = $mdb -> select( 'pp_articles_images', '*', [ 'article_id' => null ] ); - if ( is_array( $results ) ) foreach ( $results as $row ) - { - $dir = '/upload/article_images/article_' . $article_id; - - $new_file_name = str_replace( '/upload/article_images/tmp', $dir, $row['src'] ); - - if ( file_exists( '../' . $new_file_name ) ) - { - $ext = strrpos( $new_file_name, '.' ); - $fileName_a = substr( $new_file_name, 0, $ext ); - $fileName_b = substr( $new_file_name, $ext ); - - $count = 1; - - while ( file_exists( '../' . $fileName_a . '_' . $count . $fileName_b ) ) - $count++; - - $new_file_name = $fileName_a . '_' . $count . $fileName_b; - } - - if ( file_exists( '..' . $row['src'] ) ) - { - if ( !is_dir( '../' . $dir ) and $created !== true ) - { - if ( mkdir( '../' . $dir, 0755, true ) ) - $created = true; - } - rename( '..' . $row['src'], '..' . $new_file_name ); - } - - $mdb -> update( 'pp_articles_images', [ 'src' => $new_file_name, 'article_id' => (int)$article_id ], [ 'id' => $row['id'] ] ); - } - - $results = $mdb -> select( 'pp_articles_images', '*', [ 'AND' => [ 'article_id' => (int)$article_id, 'to_delete' => 1 ] ] ); - if ( is_array( $results ) ) foreach ( $results as $row ) - { - if ( file_exists( '../' . $row['src'] ) ) - unlink( '../' . $row['src'] ); - } - - $mdb -> delete( 'pp_articles_images', [ 'AND' => [ 'article_id' => (int)$article_id, 'to_delete' => 1 ] ] ); - - $results = $mdb -> select( 'pp_articles_files', '*', [ 'AND' => [ 'article_id' => (int)$article_id, 'to_delete' => 1 ] ] ); - if ( is_array( $results ) ) foreach ( $results as $row ) - { - if ( file_exists( '../' . $row['src'] ) ) - unlink( '../' . $row['src'] ); - } - - $mdb -> delete( 'pp_articles_files', [ 'AND' => [ 'article_id' => (int)$article_id, 'to_delete' => 1 ] ] ); - - \S::htacces(); - - \S::delete_dir( '../temp/' ); - - return $article_id; - } + return $repository->save( (int)$article_id, [ + 'title' => $title, 'main_image' => $main_image, 'entry' => $entry, + 'text' => $text, 'table_of_contents' => $table_of_contents, + 'status' => $status, 'show_title' => $show_title, + 'show_table_of_contents' => $show_table_of_contents, + 'show_date_add' => $show_date_add, 'date_add' => $date_add, + 'show_date_modify' => $show_date_modify, 'date_modify' => $date_modify, + 'seo_link' => $seo_link, 'meta_title' => $meta_title, + 'meta_description' => $meta_description, 'meta_keywords' => $meta_keywords, + 'layout_id' => $layout_id, 'pages' => $pages, 'noindex' => $noindex, + 'repeat_entry' => $repeat_entry, 'copy_from' => $copy_from, + 'social_icons' => $social_icons, 'block_direct_access' => $block_direct_access, + ], (int)$user['id'] ); } public static function delete_nonassigned_files() diff --git a/tests/Unit/Domain/Article/ArticleRepositoryTest.php b/tests/Unit/Domain/Article/ArticleRepositoryTest.php index b040292..44ea869 100644 --- a/tests/Unit/Domain/Article/ArticleRepositoryTest.php +++ b/tests/Unit/Domain/Article/ArticleRepositoryTest.php @@ -99,4 +99,297 @@ class ArticleRepositoryTest extends TestCase $this->assertTrue(true); } + + private function getSampleData(): array + { + return [ + 'title' => ['pl' => 'Testowy artykul', 'en' => 'Test article'], + 'main_image' => ['pl' => '/img/pl.jpg', 'en' => ''], + 'entry' => ['pl' => 'Wstep', 'en' => 'Entry'], + 'text' => ['pl' => 'Tresc', 'en' => 'Content'], + 'table_of_contents' => ['pl' => '', 'en' => ''], + 'status' => 'on', + 'show_title' => 'on', + 'show_table_of_contents' => '', + 'show_date_add' => 'on', + 'date_add' => '', + 'show_date_modify' => '', + 'date_modify' => '', + 'seo_link' => ['pl' => 'testowy-artykul', 'en' => 'test-article'], + 'meta_title' => ['pl' => 'Meta PL', 'en' => ''], + 'meta_description' => ['pl' => '', 'en' => ''], + 'meta_keywords' => ['pl' => '', 'en' => ''], + 'layout_id' => '2', + 'pages' => ['1', '3'], + 'noindex' => ['pl' => '', 'en' => 'on'], + 'repeat_entry' => '', + 'copy_from' => ['pl' => '', 'en' => ''], + 'social_icons' => 'on', + 'block_direct_access' => ['pl' => '', 'en' => ''], + ]; + } + + public function testSaveCreatesNewArticle(): void + { + $mockDb = $this->createMock(\medoo::class); + $data = $this->getSampleData(); + + $insertCalls = []; + $mockDb->method('insert') + ->willReturnCallback(function ($table, $row) use (&$insertCalls) { + $insertCalls[] = ['table' => $table, 'row' => $row]; + return true; + }); + + $mockDb->expects($this->once()) + ->method('id') + ->willReturn(42); + + $mockDb->method('select')->willReturn([]); + $mockDb->method('max')->willReturn(5); + + $repository = new ArticleRepository($mockDb); + $result = $repository->save(0, $data, 1); + + $this->assertEquals(42, $result); + + // Verify article insert + $articleInsert = $insertCalls[0]; + $this->assertEquals('pp_articles', $articleInsert['table']); + $this->assertEquals(1, $articleInsert['row']['status']); + $this->assertEquals(1, $articleInsert['row']['show_title']); + $this->assertEquals(2, $articleInsert['row']['layout_id']); + $this->assertArrayHasKey('date_add', $articleInsert['row']); + } + + public function testSaveReturnsZeroWhenInsertFails(): void + { + $mockDb = $this->createMock(\medoo::class); + + $mockDb->method('insert'); + $mockDb->method('id')->willReturn(null); + + $repository = new ArticleRepository($mockDb); + $result = $repository->save(0, $this->getSampleData(), 1); + + $this->assertEquals(0, $result); + } + + public function testSaveUpdatesExistingArticle(): void + { + $mockDb = $this->createMock(\medoo::class); + $data = $this->getSampleData(); + + $updateCalls = []; + $mockDb->method('update') + ->willReturnCallback(function ($table, $row, $where = null) use (&$updateCalls) { + $updateCalls[] = ['table' => $table, 'row' => $row, 'where' => $where]; + return true; + }); + + $mockDb->method('get')->willReturn(99); + $mockDb->method('select')->willReturn([]); + $mockDb->method('max')->willReturn(0); + $mockDb->method('insert')->willReturn(true); + + $repository = new ArticleRepository($mockDb); + $result = $repository->save(10, $data, 1); + + $this->assertEquals(10, $result); + + // Verify article update + $articleUpdate = $updateCalls[0]; + $this->assertEquals('pp_articles', $articleUpdate['table']); + $this->assertEquals(1, $articleUpdate['row']['status']); + $this->assertArrayNotHasKey('date_add', $articleUpdate['row']); + $this->assertEquals(['id' => 10], $articleUpdate['where']); + } + + public function testSaveTranslationsInsertsForNewArticle(): void + { + $mockDb = $this->createMock(\medoo::class); + $data = $this->getSampleData(); + + // 1 insert for pp_articles + 2 inserts for translations (pl, en) + 2 inserts for pages + $insertCalls = []; + $mockDb->method('insert') + ->willReturnCallback(function ($table, $row) use (&$insertCalls) { + $insertCalls[] = ['table' => $table, 'row' => $row]; + return true; + }); + + $mockDb->method('id')->willReturn(50); + $mockDb->method('select')->willReturn([]); + $mockDb->method('max')->willReturn(0); + + $repository = new ArticleRepository($mockDb); + $repository->save(0, $data, 1); + + $langInserts = array_filter($insertCalls, function ($c) { + return $c['table'] === 'pp_articles_langs'; + }); + + $this->assertCount(2, $langInserts); + + $plInsert = array_values(array_filter($langInserts, function ($c) { + return $c['row']['lang_id'] === 'pl'; + }))[0]['row']; + + $this->assertEquals(50, $plInsert['article_id']); + $this->assertEquals('Testowy artykul', $plInsert['title']); + $this->assertEquals('/img/pl.jpg', $plInsert['main_image']); + } + + public function testSaveTranslationsUpsertsForExistingArticle(): void + { + $mockDb = $this->createMock(\medoo::class); + $data = $this->getSampleData(); + + // get returns translation ID for 'pl', null for 'en' + $mockDb->method('get') + ->willReturnOnConsecutiveCalls(100, null); + + $updateCalls = []; + $mockDb->method('update') + ->willReturnCallback(function ($table, $row, $where = null) use (&$updateCalls) { + $updateCalls[] = ['table' => $table, 'row' => $row, 'where' => $where]; + return true; + }); + + $insertCalls = []; + $mockDb->method('insert') + ->willReturnCallback(function ($table, $row) use (&$insertCalls) { + $insertCalls[] = ['table' => $table, 'row' => $row]; + return true; + }); + + $mockDb->method('select')->willReturn([]); + $mockDb->method('max')->willReturn(0); + + $repository = new ArticleRepository($mockDb); + $repository->save(10, $data, 1); + + // pl should be updated (translation_id=100) + $langUpdates = array_filter($updateCalls, function ($c) { + return $c['table'] === 'pp_articles_langs'; + }); + $this->assertCount(1, $langUpdates); + + // en should be inserted (no existing translation) + $langInserts = array_filter($insertCalls, function ($c) { + return $c['table'] === 'pp_articles_langs'; + }); + $this->assertCount(1, $langInserts); + } + + public function testSavePagesForNewArticle(): void + { + $mockDb = $this->createMock(\medoo::class); + $data = $this->getSampleData(); + $data['pages'] = ['5', '8']; + + $insertCalls = []; + $mockDb->method('insert') + ->willReturnCallback(function ($table, $row) use (&$insertCalls) { + $insertCalls[] = ['table' => $table, 'row' => $row]; + return true; + }); + + $mockDb->method('id')->willReturn(60); + $mockDb->method('select')->willReturn([]); + $mockDb->method('max')->willReturn(10); + + $repository = new ArticleRepository($mockDb); + $repository->save(0, $data, 1); + + $pageInserts = array_filter($insertCalls, function ($c) { + return $c['table'] === 'pp_articles_pages'; + }); + + $this->assertCount(2, $pageInserts); + + $pageIds = array_map(function ($c) { + return $c['row']['page_id']; + }, array_values($pageInserts)); + + $this->assertContains(5, $pageIds); + $this->assertContains(8, $pageIds); + } + + public function testSaveDeletesMarkedImagesOnUpdate(): void + { + $mockDb = $this->createMock(\medoo::class); + $data = $this->getSampleData(); + $data['pages'] = null; + + $mockDb->method('update')->willReturn(true); + $mockDb->method('get')->willReturn(null); + $mockDb->method('max')->willReturn(0); + + $selectCalls = 0; + $mockDb->method('select') + ->willReturnCallback(function ($table, $columns, $where) use (&$selectCalls) { + $selectCalls++; + // Return marked images for deletion query + if ($table === 'pp_articles_images' && isset($where['AND']['to_delete'])) { + return [['id' => 1, 'src' => '/nonexistent/path/img.jpg']]; + } + if ($table === 'pp_articles_files' && isset($where['AND']['to_delete'])) { + return [['id' => 2, 'src' => '/nonexistent/path/file.pdf']]; + } + return []; + }); + + $deleteCalls = []; + $mockDb->method('delete') + ->willReturnCallback(function ($table, $where) use (&$deleteCalls) { + $deleteCalls[] = ['table' => $table, 'where' => $where]; + return true; + }); + + $mockDb->method('insert')->willReturn(true); + + $repository = new ArticleRepository($mockDb); + $repository->save(15, $data, 1); + + $imageDeletes = array_filter($deleteCalls, function ($c) { + return $c['table'] === 'pp_articles_images'; + }); + $fileDeletes = array_filter($deleteCalls, function ($c) { + return $c['table'] === 'pp_articles_files'; + }); + + $this->assertNotEmpty($imageDeletes); + $this->assertNotEmpty($fileDeletes); + } + + public function testArchiveSetsStatusToMinusOne(): void + { + $mockDb = $this->createMock(\medoo::class); + + $mockDb->expects($this->once()) + ->method('update') + ->with('pp_articles', ['status' => -1], ['id' => 25]) + ->willReturn(true); + + $repository = new ArticleRepository($mockDb); + $result = $repository->archive(25); + + $this->assertTrue($result); + } + + public function testArchiveReturnsFalseWhenUpdateFails(): void + { + $mockDb = $this->createMock(\medoo::class); + + $mockDb->expects($this->once()) + ->method('update') + ->with('pp_articles', ['status' => -1], ['id' => 999]) + ->willReturn(false); + + $repository = new ArticleRepository($mockDb); + $result = $repository->archive(999); + + $this->assertFalse($result); + } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 93fc4e2..aad8116 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -37,6 +37,7 @@ date_default_timezone_set('Europe/Warsaw'); // Stuby klas systemowych (nie dostępnych w testach unit) if (!class_exists('S')) { class S { + public static function seo($str) { return $str; } public static function delete_dir($path) {} public static function alert($msg) {} public static function htacces() {}