Lines 100.00% 90 / 90
Methods 100.00% 18 / 18
Classes 100.00% 1 / 1
Covered by tests of size
Name Lines Methods CRAP
 __construct 100.00% 2 / 2 100.00% 1 / 1 1
 getAllPages 100.00% 5 / 5 100.00% 1 / 1 2
 getPagesPaginated 100.00% 12 / 12 100.00% 1 / 1 2
 getAllLanguagesPaginated 100.00% 10 / 10 100.00% 1 / 1 2
 countPages 100.00% 2 / 2 100.00% 1 / 1 1
 countAllLanguages 100.00% 1 / 1 100.00% 1 / 1 1
 getById 100.00% 3 / 3 100.00% 1 / 1 2
 getBySlug 100.00% 3 / 3 100.00% 1 / 1 2
 getExistingLanguages 100.00% 1 / 1 100.00% 1 / 1 1
 create 100.00% 1 / 1 100.00% 1 / 1 1
 createTranslation 100.00% 1 / 1 100.00% 1 / 1 1
 update 100.00% 3 / 3 100.00% 1 / 1 2
 delete 100.00% 2 / 2 100.00% 1 / 1 1
 activate 100.00% 1 / 1 100.00% 1 / 1 1
 slugExists 100.00% 2 / 2 100.00% 1 / 1 1
 generateUniqueSlug 100.00% 7 / 7 100.00% 1 / 1 2
 mapRowToArray 100.00% 15 / 15 100.00% 1 / 1 1
 mapRowToEntity 100.00% 19 / 19 100.00% 1 / 1 5
35readonly 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<string, mixed>>
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     * Get paginated custom pages across all languages with sorting support.
107     *
108     * @param bool $activeOnly Filter by active status
109     * @param int $limit Number of items per page
110     * @param int $offset Starting offset
111     * @param string $sortField Field to sort by
112     * @param string $sortOrder Sort direction (ASC, DESC)
113     * @return array
114     */
115    public function getAllLanguagesPaginated(
116        bool $activeOnly = false,
117        int $limit = 25,
118        int $offset = 0,
119        string $sortField = 'created',
120        string $sortOrder = 'DESC',
121    ): array {
122        $pages = [];
123
124        foreach ($this->repository->getAllLanguagesPaginated(
125            $activeOnly,
126            $limit,
127            $offset,
128            $sortField,
129            $sortOrder,
130        ) as $row) {
131            $pages[] = $this->mapRowToArray($row);
132        }
133
134        return $pages;
135    }
136
137    /**
138     * Count total pages for the current language.
139     *
140     * @param bool $activeOnly Filter by active status
141     * @return int Total count
142     */
143    public function countPages(bool $activeOnly = false): int
144    {
145        $language = $this->configuration->getLanguage()->getLanguage();
146        return $this->repository->countAll($language, $activeOnly);
147    }
148
149    /**
150     * Count total pages across all languages.
151     *
152     * @param bool $activeOnly Filter by active status
153     * @return int Total count
154     */
155    public function countAllLanguages(bool $activeOnly = false): int
156    {
157        return $this->repository->countAllLanguages($activeOnly);
158    }
159
160    /**
161     * Get a custom page by ID.
162     *
163     * @param int $pageId Page ID
164     * @param string|null $language Language code (optional, uses current if not provided)
165     * @return CustomPageEntity|null Page entity or null if not found
166     */
167    public function getById(int $pageId, ?string $language = null): ?CustomPageEntity
168    {
169        $language ??= $this->configuration->getLanguage()->getLanguage();
170        $row = $this->repository->getById($pageId, $language);
171        return $row ? $this->mapRowToEntity($row) : null;
172    }
173
174    /**
175     * Get a custom page by slug.
176     *
177     * @param string $slug URL slug
178     * @param string|null $language Language code (optional, uses current if not provided)
179     * @return CustomPageEntity|null Page entity or null if not found
180     */
181    public function getBySlug(string $slug, ?string $language = null): ?CustomPageEntity
182    {
183        $language ??= $this->configuration->getLanguage()->getLanguage();
184        $row = $this->repository->getBySlug($slug, $language);
185        return $row ? $this->mapRowToEntity($row) : null;
186    }
187
188    /**
189     * Get all existing languages for a given page ID.
190     *
191     * @param int $pageId Page ID
192     * @return array<string> Array of language codes
193     */
194    public function getExistingLanguages(int $pageId): array
195    {
196        return $this->repository->getExistingLanguages($pageId);
197    }
198
199    /**
200     * Create a new custom page.
201     *
202     * @param CustomPageEntity $page Custom page entity
203     * @return int The new page ID
204     */
205    public function create(CustomPageEntity $page): int
206    {
207        return $this->repository->insert($page);
208    }
209
210    /**
211     * Create a translation for an existing custom page.
212     *
213     * @param CustomPageEntity $page Custom page entity
214     * @param int $pageId The existing page ID to use
215     * @return bool Success status
216     */
217    public function createTranslation(CustomPageEntity $page, int $pageId): bool
218    {
219        return $this->repository->insertTranslation($page, $pageId);
220    }
221
222    /**
223     * Update an existing custom page.
224     *
225     * @param CustomPageEntity $page Custom page entity
226     * @return bool Success status
227     */
228    public function update(CustomPageEntity $page): bool
229    {
230        if (!$page->getUpdated()) {
231            $page->setUpdated(new DateTime());
232        }
233        return $this->repository->update($page);
234    }
235
236    /**
237     * Delete a custom page.
238     *
239     * @param int $pageId Page ID
240     * @param string|null $language Language code (optional, uses current if not provided)
241     * @return bool Success status
242     */
243    public function delete(int $pageId, ?string $language = null): bool
244    {
245        $language ??= $this->configuration->getLanguage()->getLanguage();
246        return $this->repository->delete($pageId, $language);
247    }
248
249    /**
250     * Activate or deactivate a custom page.
251     *
252     * @param int $pageId Page ID
253     * @param bool $status Active status
254     * @return bool Success status
255     */
256    public function activate(int $pageId, bool $status): bool
257    {
258        return $this->repository->activate($pageId, $status);
259    }
260
261    /**
262     * Check if a slug exists.
263     *
264     * @param string $slug URL slug
265     * @param string|null $language Language code (optional, uses current if not provided)
266     * @param int|null $excludeId Optional page ID to exclude from check
267     * @return bool True if slug exists
268     */
269    public function slugExists(string $slug, ?string $language = null, ?int $excludeId = null): bool
270    {
271        $language ??= $this->configuration->getLanguage()->getLanguage();
272        return $this->repository->slugExists($slug, $language, $excludeId);
273    }
274
275    /**
276     * Generate a unique slug from a base string.
277     *
278     * @param string $baseSlug Base slug string
279     * @param string|null $language Language code (optional, uses current if not provided)
280     * @param int|null $excludeId Optional page ID to exclude from check
281     * @return string Unique slug
282     */
283    public function generateUniqueSlug(string $baseSlug, ?string $language = null, ?int $excludeId = null): string
284    {
285        $language ??= $this->configuration->getLanguage()->getLanguage();
286        $slug = $baseSlug;
287        $counter = 1;
288
289        while ($this->slugExists($slug, $language, $excludeId)) {
290            $slug = $baseSlug . '-' . $counter;
291            $counter++;
292        }
293
294        return $slug;
295    }
296
297    /**
298     * Map database row to array.
299     *
300     * @param stdClass $row Database row
301     * @return array<string, mixed> Mapped data
302     */
303    private function mapRowToArray(stdClass $row): array
304    {
305        return [
306            'id' => (int) $row->id,
307            'lang' => $row->lang,
308            'page_title' => $row->page_title,
309            'slug' => $row->slug,
310            'content' => $row->content,
311            'author_name' => $row->author_name,
312            'author_email' => $row->author_email,
313            'active' => $row->active,
314            'created' => $row->created,
315            'updated' => $row->updated ?? null,
316            'seo_title' => $row->seo_title ?? null,
317            'seo_description' => $row->seo_description ?? null,
318            'seo_robots' => $row->seo_robots ?? 'index,follow',
319        ];
320    }
321
322    /**
323     * Map database row to CustomPageEntity.
324     *
325     * @param stdClass $row Database row
326     * @return CustomPageEntity Page entity
327     * @throws \DateMalformedStringException
328     */
329    private function mapRowToEntity(stdClass $row): CustomPageEntity
330    {
331        $seoTitle = $row->seo_title ?? null;
332        $seoDescription = $row->seo_description ?? null;
333
334        $entity = new CustomPageEntity();
335        $entity
336            ->setId((int) $row->id)
337            ->setLanguage((string) $row->lang)
338            ->setPageTitle((string) $row->page_title)
339            ->setSlug((string) $row->slug)
340            ->setContent((string) $row->content)
341            ->setAuthorName((string) $row->author_name)
342            ->setAuthorEmail((string) $row->author_email)
343            ->setActive($row->active === 'y')
344            ->setSeoTitle($seoTitle === null ? null : (string) $seoTitle)
345            ->setSeoDescription($seoDescription === null ? null : (string) $seoDescription)
346            ->setSeoRobots((string) ($row->seo_robots ?? 'index,follow'))
347            ->setCreated(new DateTime((string) $row->created));
348
349        if (property_exists($row, 'updated') && $row->updated !== null) {
350            $entity->setUpdated(new DateTime((string) $row->updated));
351        }
352
353        return $entity;
354    }
355}