feat: Migrate article_save and article_delete to Domain Architecture

Move article save/delete logic from monolithic factory to ArticleRepository
with DI-based controller actions, following the established refactoring pattern.

- ArticleRepository: add save() with 9 private helpers, archive() method
- ArticlesController: add save() and delete() actions with DI
- Factory methods delegate to repository (backward compatibility)
- Router: add article_save/article_delete action mappings
- Old controls methods marked @deprecated
- 59 tests, 123 assertions passing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-06 19:52:22 +01:00
parent 6c21b835da
commit efd93dede3
9 changed files with 677 additions and 291 deletions

View File

@@ -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

View File

@@ -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 plik<69>w/zdj<64><6A>
- Testy repozytorium rozszerzone o czyszczenie nieprzypisanych plik<69>w/zdj<64><6A>
- 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) 🎉

View File

@@ -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.
*/

View File

@@ -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
*/

View File

@@ -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',

View File

@@ -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.' ];

View File

@@ -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()

View File

@@ -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);
}
}

View File

@@ -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() {}