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

FluidTYPO3 / flux / 12937779436

23 Jan 2025 08:47PM UTC coverage: 93.304% (+0.03%) from 93.278%
12937779436

Pull #2220

github

web-flow
Merge 284c1e761 into 6e4167452
Pull Request #2220: [FEATURE] Introduce TemplatePaths proxy

5 of 10 new or added lines in 2 files covered. (50.0%)

3 existing lines in 1 file now uncovered.

7037 of 7542 relevant lines covered (93.3%)

56.32 hits per line

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

82.58
/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\Content\TypeDefinition\FluidFileBased\DropInContentTypeDefinition;
12
use FluidTYPO3\Flux\Core;
13
use FluidTYPO3\Flux\Enum\ExtensionOption;
14
use FluidTYPO3\Flux\Enum\FormOption;
15
use FluidTYPO3\Flux\Form;
16
use FluidTYPO3\Flux\Provider\PageProvider;
17
use FluidTYPO3\Flux\Proxy\TemplatePathsProxy;
18
use FluidTYPO3\Flux\Utility\ExtensionConfigurationUtility;
19
use FluidTYPO3\Flux\Utility\ExtensionNamingUtility;
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 TYPO3\CMS\Fluid\View\TemplateView;
32
use TYPO3Fluid\Fluid\Component\Error\ChildNotFoundException;
33
use TYPO3Fluid\Fluid\View\Exception\InvalidSectionException;
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

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

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

77
        $resolvedMainTemplateIdentity = null;
24✔
78
        $resolvedSubTemplateIdentity = null;
24✔
79
        $recordDefiningMain = null;
24✔
80
        $recordDefiningSub = null;
24✔
81
        $rootLine = $this->getRootLine($pageUid);
24✔
82

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

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

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

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

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

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

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

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

293
    /**
294
     * @codeCoverageIgnore
295
     */
296
    protected function createTemplatePaths(string $registeredExtensionKey): TemplatePaths
297
    {
298
        /** @var TemplatePaths $templatePaths */
299
        $templatePaths = GeneralUtility::makeInstance(
300
            TemplatePaths::class,
301
            ExtensionNamingUtility::getExtensionKey($registeredExtensionKey)
302
        );
303
        return $templatePaths;
304
    }
305

306
    /**
307
     * @codeCoverageIgnore
308
     */
309
    protected function createViewInstance(
310
        string $extensionName,
311
        TemplatePaths $templatePaths,
312
        \SplFileInfo $file
313
    ): ViewInterface {
314
        /** @var TemplateView $view */
315
        $view = GeneralUtility::makeInstance(TemplateView::class);
316
        $view->getRenderingContext()->setTemplatePaths($templatePaths);
317
        $view->getRenderingContext()->getViewHelperVariableContainer()->addOrUpdate(
318
            FormViewHelper::SCOPE,
319
            FormViewHelper::SCOPE_VARIABLE_EXTENSIONNAME,
320
            $extensionName
321
        );
322
        $templatePaths->setTemplatePathAndFilename($file->getPathname());
323
        return $view;
324
    }
325

326
    /**
327
     * @codeCoverageIgnore
328
     */
329
    protected function getRootLine(int $pageUid): array
330
    {
331
        /** @var RootlineUtility $rootLineUtility */
332
        $rootLineUtility = GeneralUtility::makeInstance(RootlineUtility::class, $pageUid);
333
        return $rootLineUtility->get();
334
    }
335
}
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