Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
73.33% covered (warning)
73.33%
22 / 30
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
SitemapXmlService
73.33% covered (warning)
73.33%
22 / 30
75.00% covered (warning)
75.00%
3 / 4
9.21
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isEnabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 collectUrls
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
4
 generateXml
20.00% covered (danger)
20.00%
2 / 10
0.00% covered (danger)
0.00%
0 / 1
4.05
1<?php
2
3/**
4 * The XML Sitemap Service
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 <thorsten@phpmyfaq.de>
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-02-23
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Seo;
21
22use phpMyFAQ\Configuration;
23use phpMyFAQ\Core\Exception;
24use phpMyFAQ\CustomPage;
25use phpMyFAQ\Faq\Statistics as FaqStatistics;
26use phpMyFAQ\Twig\TwigWrapper;
27
28class SitemapXmlService
29{
30    private const int PMF_SITEMAP_GOOGLE_MAX_URLS = 50_000;
31
32    public function __construct(
33        private readonly Configuration $configuration,
34        private readonly FaqStatistics $faqStatistics,
35        private readonly CustomPage $customPage,
36    ) {
37    }
38
39    public function isEnabled(): bool
40    {
41        return (bool) $this->configuration->get(item: 'seo.enableXMLSitemap');
42    }
43
44    /**
45     * Collects all URLs for the sitemap from FAQs and active custom pages.
46     *
47     * @return array<int, array{loc: string, lastmod: string, priority: string}>
48     */
49    public function collectUrls(): array
50    {
51        $items = $this->faqStatistics->getTopTenData(self::PMF_SITEMAP_GOOGLE_MAX_URLS - 1);
52
53        $urls = [];
54        foreach ($items as $item) {
55            $urls[] = [
56                'loc' => $item['url'],
57                'lastmod' => $item['date'],
58                'priority' => '1.00',
59            ];
60        }
61
62        $pages = $this->customPage->getAllPages();
63
64        foreach ($pages as $page) {
65            if ($page['active'] !== 'y') {
66                continue;
67            }
68
69            $urls[] = [
70                'loc' => $this->configuration->getDefaultUrl() . 'page/' . (string) $page['slug'] . '.html',
71                'lastmod' => (string) ($page['updated'] ?? $page['created']),
72                'priority' => '0.80',
73            ];
74        }
75
76        return $urls;
77    }
78
79    /**
80     * Generates the sitemap XML content.
81     *
82     * @throws Exception|\Exception
83     * @return string|null Returns XML content or null if sitemap is disabled
84     */
85    public function generateXml(): ?string
86    {
87        if (!$this->isEnabled()) {
88            return null;
89        }
90
91        $urls = $this->collectUrls();
92
93        $twigWrapper = new TwigWrapper(
94            (string) PMF_ROOT_DIR . '/assets/templates',
95            false,
96            $this->configuration->getTemplateSet(),
97        );
98
99        $template = $twigWrapper->loadTemplate('./sitemap.xml.twig');
100
101        return $template->render(['urls' => $urls]);
102    }
103}