• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TYPO3-Headless / headless / 25563299292

08 May 2026 03:09PM UTC coverage: 74.7% (+1.7%) from 73.04%
25563299292

push

github

lukaszuznanski
[FEATURE] TYPO3 v14 compatibility (5.0.0-rc1)

Drops support for TYPO3 12/13. Bumps required PHP/TYPO3 versions,
removes deprecated PreviewController XClass, adds LanguageMenuProcessor
and assorted v14 adaptations across data processors, middlewares,
SEO, and tests.

38 of 58 new or added lines in 10 files covered. (65.52%)

23 existing lines in 2 files now uncovered.

1181 of 1581 relevant lines covered (74.7%)

6.63 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/Classes/DataProcessing/RootSiteProcessing/SiteProvider.php
1
<?php
2

3
/*
4
 * This file is part of the "headless" Extension for TYPO3 CMS.
5
 *
6
 * For the full copyright and license information, please read the
7
 * LICENSE.md file that was distributed with this source code.
8
 */
9

10
declare(strict_types=1);
11

12
namespace FriendsOfTYPO3\Headless\DataProcessing\RootSiteProcessing;
13

14
use Doctrine\DBAL\ArrayParameterType;
15
use Doctrine\DBAL\Driver\Exception;
16
use InvalidArgumentException;
17
use TYPO3\CMS\Core\Database\Connection;
18
use TYPO3\CMS\Core\Database\ConnectionPool;
19
use TYPO3\CMS\Core\Site\Entity\Site;
20
use TYPO3\CMS\Core\Site\SiteFinder;
21

22
use TYPO3\CMS\Core\Utility\GeneralUtility;
23

24
use function array_filter;
25
use function array_map;
26
use function array_values;
27
use function count;
28
use function in_array;
29
use function is_a;
30
use function usort;
31

32
class SiteProvider implements SiteProviderInterface
33
{
34
    /**
35
     * @var Site[]
36
     */
37
    private array $sites;
38
    /**
39
     * @var array[]
40
     */
41
    private array $pagesData;
42
    private Site $currentRootPage;
43

44
    public function __construct(protected ConnectionPool $connectionPool, protected SiteFinder $siteFinder) {}
45

46
    /**
47
     * @param array<string, mixed> $config
48
     * @param int $siteUid
49
     */
50
    public function prepare(array $config, int $siteUid): self
51
    {
52
        $allowedSites = $config['allowedSites'] ?? null;
×
53
        $sortingField = $config['sortingField'] ?? 'sorting';
×
54
        $customSorting = $config['sortingImplementation'] ?? null;
×
55

56
        if ($sortingField === '') {
×
57
            $sortingField = 'sorting';
×
58
        }
59

60
        if ($allowedSites === null) {
×
61
            $allowedSites = [];
×
62
        } else {
63
            $allowedSites = GeneralUtility::intExplode(',', $allowedSites, true);
×
64
        }
65

66
        $sitesFromPid = (int)($config['sitesFromPid'] ?? 0);
×
67

68
        if ($sitesFromPid) {
×
69
            $allowedSites = $this->fetchAvailableRootSitesByPid($sitesFromPid);
×
70
        }
71

72
        $sites = $this->filterSites($allowedSites);
×
73
        $pages = $this->fetchPageData($sites, $config);
×
74

75
        if ($customSorting !== null) {
×
76
            if (!is_a($customSorting, SiteSortingInterface::class, true)) {
×
77
                throw new InvalidArgumentException('Invalid implementation of SiteSortingInterface');
×
78
            }
79
            /**
80
             * @var SiteSortingInterface $sorting
81
             */
82
            $sorting = GeneralUtility::makeInstance($customSorting, $sites, $pages, $sortingField);
×
83
            $sites = $sorting->sort();
×
84
        } else {
85
            usort($sites, static function (Site $siteA, Site $siteB) use ($pages, $sortingField) {
×
86
                // phpcs:ignore Generic.Files.LineLength
87
                return (int)$pages[$siteA->getRootPageId()][$sortingField] >= (int)$pages[$siteB->getRootPageId()][$sortingField] ? 1 : -1;
×
88
            });
×
89
        }
90

91
        $this->sites = $sites;
×
92
        $this->pagesData = $pages;
×
93
        $this->currentRootPage = $this->siteFinder->getSiteByPageId($siteUid);
×
94

95
        return $this;
×
96
    }
97

98
    /**
99
     * @return array<Site>
100
     */
101
    public function getSites(): array
102
    {
103
        return $this->sites;
×
104
    }
105

106
    /**
107
     * @return array<int, array>
108
     */
109
    public function getPages(): array
110
    {
111
        return $this->pagesData;
×
112
    }
113

114
    /**
115
     * @return Site
116
     */
117
    public function getCurrentRootPage(): Site
118
    {
119
        return $this->currentRootPage;
×
120
    }
121

122
    /**
123
     * @param array<int> $allowedSites
124
     * @return array<Site>
125
     */
126
    private function filterSites(array $allowedSites = []): array
127
    {
128
        $allSites = $this->siteFinder->getAllSites();
×
129

130
        if (count($allowedSites) === 0) {
×
131
            return array_filter($allSites, static function (Site $site) {
×
132
                return $site->getConfiguration()['headless'] ?? false;
×
133
            });
×
134
        }
135

136
        $sites = [];
×
137

138
        foreach ($allSites as $site) {
×
139
            if (in_array($site->getRootPageId(), $allowedSites, true) &&
×
140
            $site->getConfiguration()['headless'] ?? false) {
×
141
                $sites[] = $site;
×
142
            }
143
        }
144

145
        return $sites;
×
146
    }
147

148
    /**
149
     * @param int $pid
150
     * @return array<int>
151
     * @throws Exception
152
     */
153
    private function fetchAvailableRootSitesByPid(int $pid): array
154
    {
155
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
×
156

157
        $pagesData = $queryBuilder
×
158
            ->select('uid')
×
159
            ->from('pages')
×
160
            ->andWhere(
×
161
                $queryBuilder->expr()->eq('is_siteroot', $queryBuilder->createNamedParameter(1, Connection::PARAM_INT)),
×
162
                $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, Connection::PARAM_INT)),
×
163
            )
×
164
            ->executeQuery()
×
165
            ->fetchAllAssociative();
×
166

167
        return array_map(static function (array $item): int {
×
168
            return (int)$item['uid'];
×
169
        }, $pagesData);
×
170
    }
171

172
    /**
173
     * Fetches pages' titles & translations (if site has more than one language) of root pages
174
     *
175
     * @param array<Site> $sites
176
     * @param array<string, mixed> $config
177
     * @return array<int, array>
178
     * @throws Exception
179
     */
180
    private function fetchPageData(array $sites, array $config = []): array
181
    {
182
        $rootPagesId = array_values(array_map(static function (Site $item) {
×
183
            return $item->getRootPageId();
×
184
        }, $sites));
×
185

186
        $columns = GeneralUtility::trimExplode(',', $config['dbColumns'] ?? 'uid,title,sorting', true);
×
187

188
        if (count($columns) === 0) {
×
189
            $columns = ['uid', 'title', 'sorting'];
×
190
        }
191

192
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
×
193

194
        $pagesData = $queryBuilder
×
195
            ->select(...$columns)
×
196
            ->from('pages')
×
197
            ->where(
×
198
                $queryBuilder->expr()->in(
×
199
                    'uid',
×
NEW
200
                    $queryBuilder->createNamedParameter($rootPagesId, ArrayParameterType::INTEGER)
×
201
                )
×
202
            )
×
203
            ->executeQuery()
×
204
            ->fetchAllAssociative();
×
205

206
        $pages = [];
×
207

208
        foreach ($pagesData as $page) {
×
209
            $pages[(int)$page['uid']] = $page;
×
210
        }
211

212
        return $pages;
×
213
    }
214
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc