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

FluidTYPO3 / flux / 18588861398

17 Oct 2025 09:40AM UTC coverage: 92.656% (-0.1%) from 92.767%
18588861398

push

github

NamelessCoder
[TER] 11.1.0

7090 of 7652 relevant lines covered (92.66%)

66.89 hits per line

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

82.05
/Classes/Service/PageService.php
1
<?php
2
namespace FluidTYPO3\Flux\Service;
3

4
/*
5
 * This file is part of the FluidTYPO3/Flux project under GPLv2 or later.
6
 *
7
 * For the full copyright and license information, please read the
8
 * LICENSE.md file that was distributed with this source code.
9
 */
10

11
use FluidTYPO3\Flux\Builder\ViewBuilder;
12
use FluidTYPO3\Flux\Content\TypeDefinition\FluidFileBased\DropInContentTypeDefinition;
13
use FluidTYPO3\Flux\Core;
14
use FluidTYPO3\Flux\Enum\ExtensionOption;
15
use FluidTYPO3\Flux\Enum\FormOption;
16
use FluidTYPO3\Flux\Form;
17
use FluidTYPO3\Flux\Provider\PageProvider;
18
use FluidTYPO3\Flux\Proxy\TemplatePathsProxy;
19
use FluidTYPO3\Flux\Utility\ExtensionConfigurationUtility;
20
use FluidTYPO3\Flux\ViewHelpers\FormViewHelper;
21
use Psr\Log\LoggerAwareInterface;
22
use Psr\Log\LoggerAwareTrait;
23
use Psr\Log\LoggerInterface;
24
use Symfony\Component\Finder\Finder;
25
use TYPO3\CMS\Core\Cache\CacheManager;
26
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
27
use TYPO3\CMS\Core\SingletonInterface;
28
use TYPO3\CMS\Core\Utility\GeneralUtility;
29
use TYPO3\CMS\Core\Utility\RootlineUtility;
30
use TYPO3\CMS\Fluid\View\TemplatePaths;
31
use TYPO3Fluid\Fluid\Component\Error\ChildNotFoundException;
32
use TYPO3Fluid\Fluid\View\Exception\InvalidSectionException;
33
use TYPO3Fluid\Fluid\View\Exception\InvalidTemplateResourceException;
34
use TYPO3Fluid\Fluid\View\ViewInterface;
35

36
/**
37
 * Page Service
38
 *
39
 * Service for interacting with Pages - gets content elements and page configuration
40
 * options.
41
 */
42
class PageService implements SingletonInterface, LoggerAwareInterface
43
{
44
    use LoggerAwareTrait;
45

46
    protected WorkspacesAwareRecordService $workspacesAwareRecordService;
47
    protected FrontendInterface $runtimeCache;
48
    protected ViewBuilder $viewBuilder;
49

50
    public function __construct(
51
        WorkspacesAwareRecordService $recordService,
52
        CacheManager $cacheManager,
53
        ViewBuilder $viewBuilder
54
    ) {
55
        $this->workspacesAwareRecordService = $recordService;
×
56
        $this->runtimeCache = $cacheManager->getCache('runtime');
×
57
        $this->viewBuilder = $viewBuilder;
×
58
    }
59

60
    /**
61
     * Process RootLine to find first usable, configured Fluid Page Template.
62
     * WARNING: do NOT use the output of this feature to overwrite $row - the
63
     * record returned may or may not be the same record as defined in $id.
64
     *
65
     * @api
66
     */
67
    public function getPageTemplateConfiguration(int $pageUid, bool $pageUidIsParentUid = false): ?array
68
    {
69
        $pageUid = (integer) $pageUid;
35✔
70
        if (!$pageUid) {
35✔
71
            return null;
7✔
72
        }
73
        $cacheId = 'flux-template-configuration-' . $pageUid;
28✔
74
        /** @var array|null $fromCache */
75
        $fromCache = $this->runtimeCache->get($cacheId);
28✔
76
        if ($fromCache) {
28✔
77
            return $fromCache;
×
78
        }
79

80
        $resolvedMainTemplateIdentity = null;
28✔
81
        $resolvedSubTemplateIdentity = null;
28✔
82
        $recordDefiningMain = null;
28✔
83
        $recordDefiningSub = null;
28✔
84
        $rootLine = $this->getRootLine($pageUid);
28✔
85

86
        // Initialize with possibly-empty values and loop root line
87
        // to fill values as they are detected.
88
        foreach ($rootLine as $page) {
28✔
89
            $rootLinePageUid = (integer) ($page['uid'] ?? 0);
28✔
90
            $mainFieldValue = $page[PageProvider::FIELD_ACTION_MAIN] ?? null;
28✔
91
            $subFieldValue = $page[PageProvider::FIELD_ACTION_SUB] ?? null;
28✔
92
            $resolvedMainTemplateIdentity = is_array($mainFieldValue) ? $mainFieldValue[0] : $mainFieldValue;
28✔
93
            $resolvedSubTemplateIdentity = is_array($subFieldValue) ? $subFieldValue[0] : $subFieldValue;
28✔
94
            $containsSubDefinition = (strpos($subFieldValue ?? '', '->') !== false);
28✔
95
            $isCandidate = $pageUidIsParentUid ? true : $rootLinePageUid !== $pageUid;
28✔
96
            if ($containsSubDefinition && $isCandidate) {
28✔
97
                $resolvedSubTemplateIdentity = $subFieldValue;
14✔
98
                $recordDefiningSub = $page;
14✔
99
                if (empty($resolvedMainTemplateIdentity)) {
14✔
100
                    // Conditions met: current page is not $pageUid, original page did not
101
                    // contain a "this page" layout, current rootline page has "sub" selection.
102
                    // Then, set our "this page" value to use the "sub" selection that was detected.
103
                    $resolvedMainTemplateIdentity = $resolvedSubTemplateIdentity;
7✔
104
                    $recordDefiningMain = $page;
7✔
105
                }
106
                break;
14✔
107
            }
108
        };
109
        if (empty($resolvedMainTemplateIdentity) && empty($resolvedSubTemplateIdentity)) {
28✔
110
            // Neither directly configured "this page" nor inherited "sub" contains a valid value;
111
            // no configuration was detected at all.
112
            return null;
14✔
113
        }
114
        $configuration = [
14✔
115
            'tx_fed_page_controller_action' => $resolvedMainTemplateIdentity,
14✔
116
            'tx_fed_page_controller_action_sub' => $resolvedSubTemplateIdentity,
14✔
117
            'record_main' => $recordDefiningMain,
14✔
118
            'record_sub' => $recordDefiningSub,
14✔
119
        ];
14✔
120
        $this->runtimeCache->set($cacheId, $configuration);
14✔
121
        return $configuration;
14✔
122
    }
123

124
    /**
125
     * Get a usable page configuration flexform from rootline
126
     *
127
     * @api
128
     */
129
    public function getPageFlexFormSource(int $pageUid): ?string
130
    {
131
        $pageUid = (integer) $pageUid;
14✔
132
        if (!$pageUid) {
14✔
133
            return null;
7✔
134
        }
135
        $fieldList = 'uid,pid,t3ver_oid,tx_fed_page_flexform';
7✔
136
        $page = $this->workspacesAwareRecordService->getSingle('pages', $fieldList, $pageUid);
7✔
137
        while ($page !== null && 0 !== (integer) $page['uid'] && empty($page['tx_fed_page_flexform'])) {
7✔
138
            $resolveParentPageUid = (integer) (0 > $page['pid'] ? $page['t3ver_oid'] : $page['pid']);
7✔
139
            $page = $this->workspacesAwareRecordService->getSingle('pages', $fieldList, $resolveParentPageUid);
7✔
140
        }
141
        return $page['tx_fed_page_flexform'] ?? null;
7✔
142
    }
143

144
    public function getPageConfiguration(?string $extensionName = null): array
145
    {
146
        if (null !== $extensionName && true === empty($extensionName)) {
49✔
147
            // Note: a NULL extensionName means "fetch ALL defined collections" whereas
148
            // an empty value that is not null indicates an incorrect caller. Instead
149
            // of returning ALL paths here, an empty array is the proper return value.
150
            // However, dispatch a debug message to inform integrators of the problem.
151
            if ($this->logger instanceof LoggerInterface) {
14✔
152
                $this->logger->log(
×
153
                    'notice',
×
154
                    'Template paths have been attempted fetched using an empty value that is NOT NULL in ' .
×
155
                    get_class($this) . '. This indicates a potential problem with your TypoScript configuration - a ' .
×
156
                    'value which is expected to be an array may be defined as a string. This error is not fatal but ' .
×
157
                    'may prevent the affected collection (which cannot be identified here) from showing up'
×
158
                );
×
159
            }
160
            return [];
14✔
161
        }
162

163
        $plugAndPlayEnabled = ExtensionConfigurationUtility::getOption(
35✔
164
            ExtensionOption::OPTION_PLUG_AND_PLAY
35✔
165
        );
35✔
166
        $plugAndPlayDirectory = ExtensionConfigurationUtility::getOption(
35✔
167
            ExtensionOption::OPTION_PLUG_AND_PLAY_DIRECTORY
35✔
168
        );
35✔
169
        if (!is_scalar($plugAndPlayDirectory)) {
35✔
170
            return [];
7✔
171
        }
172
        $plugAndPlayTemplatesDirectory = trim((string) $plugAndPlayDirectory, '/.') . '/';
28✔
173
        if ($plugAndPlayEnabled && $extensionName === 'Flux') {
28✔
174
            return [
7✔
175
                TemplatePaths::CONFIG_TEMPLATEROOTPATHS => [
7✔
176
                    $plugAndPlayTemplatesDirectory
7✔
177
                    . DropInContentTypeDefinition::TEMPLATES_DIRECTORY
7✔
178
                    . DropInContentTypeDefinition::PAGE_DIRECTORY
7✔
179
                ],
7✔
180
                TemplatePaths::CONFIG_PARTIALROOTPATHS => [
7✔
181
                    $plugAndPlayTemplatesDirectory . DropInContentTypeDefinition::PARTIALS_DIRECTORY
7✔
182
                ],
7✔
183
                TemplatePaths::CONFIG_LAYOUTROOTPATHS => [
7✔
184
                    $plugAndPlayTemplatesDirectory . DropInContentTypeDefinition::LAYOUTS_DIRECTORY
7✔
185
                ],
7✔
186
            ];
7✔
187
        }
188
        if (null !== $extensionName) {
21✔
189
            $templatePaths = $this->viewBuilder->buildTemplatePaths($extensionName);
7✔
190
            return TemplatePathsProxy::toArray($templatePaths);
7✔
191
        }
192
        $configurations = [];
14✔
193
        $registeredExtensionKeys = Core::getRegisteredProviderExtensionKeys('Page');
14✔
194
        foreach ($registeredExtensionKeys as $registeredExtensionKey) {
14✔
195
            $templatePaths = $this->viewBuilder->buildTemplatePaths($registeredExtensionKey);
14✔
196
            $configurations[$registeredExtensionKey] = TemplatePathsProxy::toArray($templatePaths);
14✔
197
        }
198
        if ($plugAndPlayEnabled) {
14✔
199
            $configurations['FluidTYPO3.Flux'] = array_replace(
7✔
200
                $configurations['FluidTYPO3.Flux'] ?? [],
7✔
201
                [
7✔
202
                    TemplatePaths::CONFIG_TEMPLATEROOTPATHS => [
7✔
203
                        $plugAndPlayTemplatesDirectory
7✔
204
                        . DropInContentTypeDefinition::TEMPLATES_DIRECTORY
7✔
205
                        . DropInContentTypeDefinition::PAGE_DIRECTORY
7✔
206
                    ],
7✔
207
                    TemplatePaths::CONFIG_PARTIALROOTPATHS => [
7✔
208
                        $plugAndPlayTemplatesDirectory . DropInContentTypeDefinition::PARTIALS_DIRECTORY
7✔
209
                    ],
7✔
210
                    TemplatePaths::CONFIG_LAYOUTROOTPATHS => [
7✔
211
                        $plugAndPlayTemplatesDirectory . DropInContentTypeDefinition::LAYOUTS_DIRECTORY
7✔
212
                    ],
7✔
213
                ]
7✔
214
            );
7✔
215
        }
216
        return $configurations;
14✔
217
    }
218

219
    /**
220
     * Gets a list of usable Page Templates from defined page template TypoScript.
221
     * Returns a list of Form instances indexed by the path ot the template file.
222
     *
223
     * @return Form[][]
224
     * @api
225
     */
226
    public function getAvailablePageTemplateFiles(): array
227
    {
228
        $cacheKey = 'page_templates';
35✔
229
        /** @var array|null $fromCache */
230
        $fromCache = $this->runtimeCache->get($cacheKey);
35✔
231
        if ($fromCache) {
35✔
232
            return $fromCache;
×
233
        }
234
        $typoScript = $this->getPageConfiguration();
35✔
235
        $output = [];
35✔
236

237
        foreach ((array) $typoScript as $extensionName => $group) {
35✔
238
            if (!($group['enable'] ?? true)) {
28✔
239
                continue;
7✔
240
            }
241
            $output[$extensionName] = [];
21✔
242
            $templatePaths = $this->viewBuilder->buildTemplatePaths($extensionName);
21✔
243
            $finder = Finder::create()->in($templatePaths->getTemplateRootPaths())->name('*.html')->sortByName();
21✔
244
            foreach ($finder->files() as $file) {
21✔
245
                /** @var \SplFileInfo $file */
246
                if (substr($file->getBasename(), 0, 1) === '.') {
21✔
247
                    continue;
×
248
                }
249
                if (strpos($file->getPath(), DIRECTORY_SEPARATOR . 'Page') === false) {
21✔
250
                    continue;
21✔
251
                }
252
                $filename = $file->getRelativePathname();
21✔
253
                if (isset($output[$extensionName][$filename])) {
21✔
254
                    continue;
×
255
                }
256

257
                $view = $this->createViewInstance($extensionName, clone $templatePaths, $file);
21✔
258
                try {
259
                    $view->renderSection('Configuration');
21✔
260
                    $form = $view->getRenderingContext()
21✔
261
                        ->getViewHelperVariableContainer()
21✔
262
                        ->get(FormViewHelper::class, 'form');
21✔
263

264
                    if (!$form instanceof Form) {
21✔
265
                        if ($this->logger instanceof LoggerInterface) {
21✔
266
                            $this->logger->log(
21✔
267
                                'error',
21✔
268
                                'Template file ' . $file . ' contains an unparsable Form definition'
21✔
269
                            );
21✔
270
                        }
271
                        continue;
21✔
272
                    } elseif (!$form->getEnabled()) {
×
273
                        if ($this->logger instanceof LoggerInterface) {
×
274
                            $this->logger->log(
×
275
                                'notice',
×
276
                                'Template file ' . $file . ' is disabled by configuration'
×
277
                            );
×
278
                        }
279
                        continue;
×
280
                    }
281
                    $form->setOption(FormOption::TEMPLATE_FILE, $file->getPathname());
×
282
                    $form->setOption(FormOption::TEMPLATE_FILE_RELATIVE, substr($file->getRelativePathname(), 5, -5));
×
283
                    $form->setExtensionName($extensionName);
×
284
                    $output[$extensionName][$filename] = $form;
×
285
                } catch (InvalidSectionException | ChildNotFoundException | InvalidTemplateResourceException $error) {
×
286
                    if ($this->logger instanceof LoggerInterface) {
×
287
                        $this->logger->log('error', $error->getMessage());
×
288
                    }
289
                }
290
            }
291
        }
292
        $this->runtimeCache->set($cacheKey, $output);
35✔
293
        return $output;
35✔
294
    }
295

296
    /**
297
     * @codeCoverageIgnore
298
     */
299
    protected function createViewInstance(
300
        string $extensionName,
301
        TemplatePaths $templatePaths,
302
        \SplFileInfo $file
303
    ): ViewInterface {
304
        $templatePaths->setTemplatePathAndFilename($file->getPathname());
305
        $view = $this->viewBuilder->buildTemplateView($extensionName, 'Page', 'default', 'Page', $file->getPathname());
306
        $view->getRenderingContext()->setTemplatePaths($templatePaths);
307
        $view->getRenderingContext()->getViewHelperVariableContainer()->addOrUpdate(
308
            FormViewHelper::SCOPE,
309
            FormViewHelper::SCOPE_VARIABLE_EXTENSIONNAME,
310
            $extensionName
311
        );
312
        return $view;
313
    }
314

315
    /**
316
     * @codeCoverageIgnore
317
     */
318
    protected function getRootLine(int $pageUid): array
319
    {
320
        /** @var RootlineUtility $rootLineUtility */
321
        $rootLineUtility = GeneralUtility::makeInstance(RootlineUtility::class, $pageUid);
322
        return $rootLineUtility->get();
323
    }
324
}
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