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

FluidTYPO3 / flux / 17432319284

03 Sep 2025 11:41AM UTC coverage: 92.919% (-0.3%) from 93.21%
17432319284

push

github

NamelessCoder
[TER] 11.0.4

7086 of 7626 relevant lines covered (92.92%)

66.03 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\ViewInterface;
34

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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