ver. 0.280: Articles frontend migration, class.Article removal, Settings facade cleanup

- Add 8 frontend methods to ArticleRepository (with Redis cache)
- Create front\Views\Articles (rendering + utility methods)
- Rewire front\view\Site::show() and front\controls\Site::route() to repo + Views
- Update 5 article templates to use \front\Views\Articles::
- Convert front\factory\Articles and front\view\Articles to facades
- Remove class.Article (entity + static methods migrated to repo + Views)
- Remove front\factory\Settings facade (already migrated)
- Fix: eliminate global $lang from articleNoindex(), inline page sort query
- Tests: 450 OK, 1431 assertions (+13 new)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 15:52:03 +01:00
parent 3b32ea0b9b
commit 723cb1a5eb
35 changed files with 1070 additions and 642 deletions

View File

@@ -683,6 +683,292 @@ class ArticleRepositoryTest extends TestCase
$this->assertMatchesRegularExpression('/LIMIT\s+100\s+OFFSET\s+0/i', $dataSql);
}
// =========================================================================
// FRONTEND METHODS
// =========================================================================
public function testArticleDetailsFrontendReturnsArticleWithRelations(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->once())
->method('get')
->with('pp_articles', '*', ['id' => 5])
->willReturn(['id' => 5, 'status' => 1, 'show_title' => 1]);
$mockDb->expects($this->exactly(4))
->method('select')
->willReturnOnConsecutiveCalls(
[['lang_id' => 'pl', 'title' => 'Testowy', 'copy_from' => null]],
[['id' => 10, 'src' => '/img/a.jpg']],
[['id' => 20, 'src' => '/files/a.pdf']],
[1, 2]
);
$repo = new ArticleRepository($mockDb);
$article = $repo->articleDetailsFrontend(5, 'pl');
$this->assertIsArray($article);
$this->assertEquals(5, $article['id']);
$this->assertArrayHasKey('language', $article);
$this->assertEquals('Testowy', $article['language']['title']);
$this->assertCount(1, $article['images']);
$this->assertCount(1, $article['files']);
}
public function testArticleDetailsFrontendReturnsNullForMissing(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->once())
->method('get')
->willReturn(false);
$repo = new ArticleRepository($mockDb);
$this->assertNull($repo->articleDetailsFrontend(999, 'pl'));
}
public function testArticleDetailsFrontendCopyFromFallback(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->method('get')
->willReturn(['id' => 7, 'status' => 1]);
$mockDb->expects($this->exactly(5))
->method('select')
->willReturnOnConsecutiveCalls(
// First call: langs with copy_from
[['lang_id' => 'en', 'title' => 'English', 'copy_from' => 'pl']],
// Second call: copy_from fallback
[['lang_id' => 'pl', 'title' => 'Polski']],
// images
[],
// files
[],
// pages
[]
);
$repo = new ArticleRepository($mockDb);
$article = $repo->articleDetailsFrontend(7, 'en');
$this->assertEquals('Polski', $article['language']['title']);
}
private function createFetchAllMock(array $data): object
{
return new class($data) {
private $data;
public function __construct($data) { $this->data = $data; }
public function fetchAll($mode = null) { return $this->data; }
};
}
public function testArticlesIdsReturnsSortedIds(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->once())
->method('query')
->willReturn($this->createFetchAllMock([
['id' => 3],
['id' => 7],
['id' => 1],
]));
$repo = new ArticleRepository($mockDb);
$result = $repo->articlesIds(1, 'pl', 10, 1, 0);
$this->assertEquals([3, 7, 1], $result);
}
public function testArticlesIdsReturnsNullForEmpty(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->method('query')
->willReturn($this->createFetchAllMock([]));
$repo = new ArticleRepository($mockDb);
$this->assertNull($repo->articlesIds(1, 'pl', 10, 0, 0));
}
public function testPageArticlesCountReturnsInt(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->once())
->method('query')
->willReturn($this->createFetchAllMock([[12]]));
$repo = new ArticleRepository($mockDb);
$this->assertSame(12, $repo->pageArticlesCount(5, 'pl'));
}
public function testPageArticlesCountReturnsZeroForEmpty(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->method('query')
->willReturn($this->createFetchAllMock([]));
$repo = new ArticleRepository($mockDb);
$this->assertSame(0, $repo->pageArticlesCount(5, 'pl'));
}
public function testPageArticlesPagination(): void
{
$mockDb = $this->createMock(\medoo::class);
// pageArticlesCount query returns 25 articles
// articlesIds query returns 10 article IDs
$mockDb->method('query')
->willReturnOnConsecutiveCalls(
$this->createFetchAllMock([[25]]),
$this->createFetchAllMock([
['id' => 11], ['id' => 12], ['id' => 13], ['id' => 14], ['id' => 15],
['id' => 16], ['id' => 17], ['id' => 18], ['id' => 19], ['id' => 20],
])
);
$page = ['id' => 3, 'articles_limit' => 10, 'sort_type' => 1];
$repo = new ArticleRepository($mockDb);
$result = $repo->pageArticles($page, 'pl', 2);
$this->assertArrayHasKey('articles', $result);
$this->assertArrayHasKey('ls', $result);
$this->assertSame(3, $result['ls']); // ceil(25/10) = 3
$this->assertCount(10, $result['articles']);
}
public function testArticleNoindexReturnsBool(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->once())
->method('get')
->with('pp_articles_langs', 'noindex', [
'AND' => ['article_id' => 5, 'lang_id' => 'pl']
])
->willReturn(1);
$repo = new ArticleRepository($mockDb);
$this->assertTrue($repo->articleNoindex(5, 'pl'));
}
public function testArticleNoindexReturnsFalseForNonNoindex(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->method('get')
->willReturn(null);
$repo = new ArticleRepository($mockDb);
$this->assertFalse($repo->articleNoindex(5, 'pl'));
}
public function testNewsReturnsArticlesArray(): void
{
$mockDb = $this->createMock(\medoo::class);
// First get() for sort_type, then get() for article_details
$mockDb->method('get')
->willReturnOnConsecutiveCalls(
1, // sort_type
['id' => 10, 'status' => 1] // article data
);
// articlesIds query returns [10]
$mockDb->method('query')
->willReturn($this->createFetchAllMock([['id' => 10]]));
// article details selects
$mockDb->method('select')
->willReturnOnConsecutiveCalls(
[['lang_id' => 'pl', 'title' => 'News', 'copy_from' => null]],
[],
[],
[]
);
$repo = new ArticleRepository($mockDb);
$result = $repo->news(3, 6, 'pl');
$this->assertIsArray($result);
$this->assertCount(1, $result);
$this->assertEquals(10, $result[0]['id']);
}
public function testTopArticlesOrderByViews(): void
{
$mockDb = $this->createMock(\medoo::class);
$queryCalls = 0;
$mockDb->method('query')
->willReturnCallback(function ($sql) use (&$queryCalls) {
$queryCalls++;
if ($queryCalls === 1) {
$this->assertStringContainsString('views DESC', $sql);
return $this->createFetchAllMock([
['id' => 5, 'date_add' => '2025-01-01', 'views' => 100, 'title' => 'Popular'],
]);
}
return $this->createFetchAllMock([]);
});
$mockDb->method('get')
->willReturn(['id' => 5, 'status' => 1]);
$mockDb->method('select')
->willReturnOnConsecutiveCalls(
[['lang_id' => 'pl', 'title' => 'Popular', 'copy_from' => null]],
[],
[],
[]
);
$repo = new ArticleRepository($mockDb);
$result = $repo->topArticles(3, 6, 'pl');
$this->assertIsArray($result);
$this->assertCount(1, $result);
}
public function testNewsListArticlesOrderByDateDesc(): void
{
$mockDb = $this->createMock(\medoo::class);
$queryCalls = 0;
$mockDb->method('query')
->willReturnCallback(function ($sql) use (&$queryCalls) {
$queryCalls++;
if ($queryCalls === 1) {
$this->assertStringContainsString('date_add DESC', $sql);
return $this->createFetchAllMock([
['id' => 8, 'date_add' => '2025-06-15', 'title' => 'Newest'],
]);
}
return $this->createFetchAllMock([]);
});
$mockDb->method('get')
->willReturn(['id' => 8, 'status' => 1]);
$mockDb->method('select')
->willReturnOnConsecutiveCalls(
[['lang_id' => 'pl', 'title' => 'Newest', 'copy_from' => null]],
[],
[],
[]
);
$repo = new ArticleRepository($mockDb);
$result = $repo->newsListArticles(3, 6, 'pl');
$this->assertIsArray($result);
$this->assertCount(1, $result);
}
public function testListForAdminUsesBoundParamsForTitleFilter(): void
{
$mockDb = $this->createMock(\medoo::class);

View File

@@ -55,6 +55,7 @@ if (!class_exists('S')) {
public static function send_email($to, $subject, $body) { return true; }
public static function remove_special_chars($str) { return str_ireplace(['\'', '"', ',', ';', '<', '>'], ' ', $str); }
public static function normalize_decimal($val, $precision = 2) { return round((float)$val, $precision); }
public static function is_array_fix($value) { return is_array($value) && count($value); }
}
}