Skip to content

Commit 1e02f8b

Browse files
committed
feat(custom-pages): added service and repository classes for custom pages (#3015)
1 parent 3fd39d6 commit 1e02f8b

22 files changed

+1817
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ This is a log of major user-visible changes in each phpMyFAQ release.
1313
- added API for glossary definitions (Thorsten)
1414
- added admin log CSV export feature (Thorsten)
1515
- added pagination, sorting, and filtering for APIs (Thorsten)
16+
- WIP: added support for custom pages in the frontend (Thorsten)
1617
- improved audit and activity log with comprehensive security event tracking (Thorsten)
1718
- improved API errors with formatted RFC 7807 Problem Details JSON responses (Thorsten)
1819
- migrated codebase to use PHP 8.4 language features (Thorsten)
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
<?php
2+
3+
/**
4+
* The CustomPage class for phpMyFAQ custom pages.
5+
*
6+
* This Source Code Form is subject to the terms of the Mozilla Public License,
7+
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
8+
* obtain one at https://mozilla.org/MPL/2.0/.
9+
*
10+
* @package phpMyFAQ
11+
* @author Thorsten Rinne <[email protected]>
12+
* @copyright 2026 phpMyFAQ Team
13+
* @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14+
* @link https://www.phpmyfaq.de
15+
* @since 2026-01-12
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace phpMyFAQ;
21+
22+
use DateTime;
23+
use phpMyFAQ\CustomPage\CustomPageRepository;
24+
use phpMyFAQ\CustomPage\CustomPageRepositoryInterface;
25+
use phpMyFAQ\Entity\CustomPageEntity;
26+
use phpMyFAQ\Seo\SeoRepository;
27+
use phpMyFAQ\Seo\SeoRepositoryInterface;
28+
use stdClass;
29+
30+
/**
31+
* Class CustomPage
32+
*
33+
* @package phpMyFAQ
34+
*/
35+
readonly class CustomPage
36+
{
37+
private CustomPageRepositoryInterface $repository;
38+
39+
private SeoRepositoryInterface $seoRepository;
40+
41+
/**
42+
* Constructor.
43+
*/
44+
public function __construct(
45+
private Configuration $configuration,
46+
?CustomPageRepositoryInterface $repository = null,
47+
?SeoRepositoryInterface $seoRepository = null,
48+
) {
49+
$this->repository = $repository ?? new CustomPageRepository($this->configuration);
50+
$this->seoRepository = $seoRepository ?? new SeoRepository($this->configuration);
51+
}
52+
53+
/**
54+
* Get all custom pages for the current language.
55+
*
56+
* @param bool $activeOnly Filter by active status
57+
* @return array<int, array>
58+
*/
59+
public function getAllPages(bool $activeOnly = false): array
60+
{
61+
$pages = [];
62+
$language = $this->configuration->getLanguage()->getLanguage();
63+
64+
foreach ($this->repository->getAll($language, $activeOnly) as $row) {
65+
$pages[] = $this->mapRowToArray($row);
66+
}
67+
68+
return $pages;
69+
}
70+
71+
/**
72+
* Get paginated custom pages with sorting support.
73+
*
74+
* @param bool $activeOnly Filter by active status
75+
* @param int $limit Number of items per page
76+
* @param int $offset Starting offset
77+
* @param string $sortField Field to sort by
78+
* @param string $sortOrder Sort direction (ASC, DESC)
79+
* @return array
80+
*/
81+
public function getPagesPaginated(
82+
bool $activeOnly = false,
83+
int $limit = 25,
84+
int $offset = 0,
85+
string $sortField = 'created',
86+
string $sortOrder = 'DESC',
87+
): array {
88+
$pages = [];
89+
$language = $this->configuration->getLanguage()->getLanguage();
90+
91+
foreach ($this->repository->getAllPaginated(
92+
$language,
93+
$activeOnly,
94+
$limit,
95+
$offset,
96+
$sortField,
97+
$sortOrder,
98+
) as $row) {
99+
$pages[] = $this->mapRowToArray($row);
100+
}
101+
102+
return $pages;
103+
}
104+
105+
/**
106+
* Count total pages for the current language.
107+
*
108+
* @param bool $activeOnly Filter by active status
109+
* @return int Total count
110+
*/
111+
public function countPages(bool $activeOnly = false): int
112+
{
113+
$language = $this->configuration->getLanguage()->getLanguage();
114+
return $this->repository->countAll($language, $activeOnly);
115+
}
116+
117+
/**
118+
* Get a custom page by ID.
119+
*
120+
* @param int $pageId Page ID
121+
* @param string|null $language Language code (optional, uses current if not provided)
122+
* @return array|null Page data or null if not found
123+
*/
124+
public function getById(int $pageId, ?string $language = null): ?array
125+
{
126+
$language = $language ?? $this->configuration->getLanguage()->getLanguage();
127+
$row = $this->repository->getById($pageId, $language);
128+
129+
return $row ? $this->mapRowToArray($row) : null;
130+
}
131+
132+
/**
133+
* Get a custom page by slug.
134+
*
135+
* @param string $slug URL slug
136+
* @param string|null $language Language code (optional, uses current if not provided)
137+
* @return array|null Page data or null if not found
138+
*/
139+
public function getBySlug(string $slug, ?string $language = null): ?array
140+
{
141+
$language = $language ?? $this->configuration->getLanguage()->getLanguage();
142+
$row = $this->repository->getBySlug($slug, $language);
143+
144+
return $row ? $this->mapRowToArray($row) : null;
145+
}
146+
147+
/**
148+
* Create a new custom page.
149+
*
150+
* @param CustomPageEntity $page Custom page entity
151+
* @return int The new page ID
152+
*/
153+
public function create(CustomPageEntity $page): int
154+
{
155+
return $this->repository->insert($page);
156+
}
157+
158+
/**
159+
* Update an existing custom page.
160+
*
161+
* @param CustomPageEntity $page Custom page entity
162+
* @return bool Success status
163+
*/
164+
public function update(CustomPageEntity $page): bool
165+
{
166+
if (!$page->getUpdated()) {
167+
$page->setUpdated(new DateTime());
168+
}
169+
return $this->repository->update($page);
170+
}
171+
172+
/**
173+
* Delete a custom page.
174+
*
175+
* @param int $pageId Page ID
176+
* @param string|null $language Language code (optional, uses current if not provided)
177+
* @return bool Success status
178+
*/
179+
public function delete(int $pageId, ?string $language = null): bool
180+
{
181+
$language = $language ?? $this->configuration->getLanguage()->getLanguage();
182+
return $this->repository->delete($pageId, $language);
183+
}
184+
185+
/**
186+
* Activate or deactivate a custom page.
187+
*
188+
* @param int $pageId Page ID
189+
* @param bool $status Active status
190+
* @return bool Success status
191+
*/
192+
public function activate(int $pageId, bool $status): bool
193+
{
194+
return $this->repository->activate($pageId, $status);
195+
}
196+
197+
/**
198+
* Check if a slug exists.
199+
*
200+
* @param string $slug URL slug
201+
* @param string|null $language Language code (optional, uses current if not provided)
202+
* @param int|null $excludeId Optional page ID to exclude from check
203+
* @return bool True if slug exists
204+
*/
205+
public function slugExists(string $slug, ?string $language = null, ?int $excludeId = null): bool
206+
{
207+
$language = $language ?? $this->configuration->getLanguage()->getLanguage();
208+
return $this->repository->slugExists($slug, $language, $excludeId);
209+
}
210+
211+
/**
212+
* Generate a unique slug from a base string.
213+
*
214+
* @param string $baseSlug Base slug string
215+
* @param string|null $language Language code (optional, uses current if not provided)
216+
* @param int|null $excludeId Optional page ID to exclude from check
217+
* @return string Unique slug
218+
*/
219+
public function generateUniqueSlug(string $baseSlug, ?string $language = null, ?int $excludeId = null): string
220+
{
221+
$language = $language ?? $this->configuration->getLanguage()->getLanguage();
222+
$slug = $baseSlug;
223+
$counter = 1;
224+
225+
while ($this->slugExists($slug, $language, $excludeId)) {
226+
$slug = $baseSlug . '-' . $counter;
227+
$counter++;
228+
}
229+
230+
return $slug;
231+
}
232+
233+
/**
234+
* Map database row to array.
235+
*
236+
* @param stdClass $row Database row
237+
* @return array Mapped data
238+
*/
239+
private function mapRowToArray(stdClass $row): array
240+
{
241+
return [
242+
'id' => (int) $row->id,
243+
'lang' => $row->lang,
244+
'page_title' => $row->page_title,
245+
'slug' => $row->slug,
246+
'content' => $row->content,
247+
'author_name' => $row->author_name,
248+
'author_email' => $row->author_email,
249+
'active' => $row->active,
250+
'created' => $row->created,
251+
'updated' => $row->updated ?? null,
252+
];
253+
}
254+
}

0 commit comments

Comments
 (0)