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

FluidTYPO3 / vhs / 27842785131

19 Jun 2026 06:40PM UTC coverage: 70.695% (-0.005%) from 70.7%
27842785131

push

github

NamelessCoder
[TASK] Correct failing test expectations

5 of 7 new or added lines in 4 files covered. (71.43%)

215 existing lines in 10 files now uncovered.

5517 of 7804 relevant lines covered (70.69%)

14.92 hits per line

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

24.82
/Classes/ViewHelpers/Page/LanguageMenuViewHelper.php
1
<?php
2
namespace FluidTYPO3\Vhs\ViewHelpers\Page;
3

4
/*
5
 * This file is part of the FluidTYPO3/Vhs 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\Vhs\Proxy\DoctrineQueryProxy;
12
use FluidTYPO3\Vhs\Proxy\SiteFinderProxy;
13
use FluidTYPO3\Vhs\Traits\ArrayConsumingViewHelperTrait;
14
use FluidTYPO3\Vhs\Traits\TagViewHelperCompatibility;
15
use FluidTYPO3\Vhs\Utility\ContentObjectFetcher;
16
use FluidTYPO3\Vhs\Utility\CoreUtility;
17
use TYPO3\CMS\Core\Context\Context;
18
use TYPO3\CMS\Core\Context\LanguageAspect;
19
use TYPO3\CMS\Core\Database\ConnectionPool;
20
use TYPO3\CMS\Core\Imaging\Icon;
21
use TYPO3\CMS\Core\Imaging\IconFactory;
22
use TYPO3\CMS\Core\Site\Site;
23
use TYPO3\CMS\Core\Utility\GeneralUtility;
24
use TYPO3\CMS\Core\Utility\VersionNumberUtility;
25
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
26
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
27
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
28
use TYPO3Fluid\Fluid\Core\ViewHelper\Exception;
29

30
/**
31
 * ViewHelper for rendering TYPO3 menus in Fluid
32
 * Require the extension static_info_table.
33
 */
34
class LanguageMenuViewHelper extends AbstractTagBasedViewHelper
35
{
36
    use ArrayConsumingViewHelperTrait;
37
    use TagViewHelperCompatibility;
38

39
    protected array $languageMenu = [];
40
    protected int $defaultLangUid = 0;
41

42
    /**
43
     * @var string
44
     */
45
    protected $tagName = 'ul';
46

47
    /**
48
     * @var ContentObjectRenderer
49
     */
50
    protected $cObj;
51

52
    protected ConfigurationManagerInterface $configurationManager;
53

54
    /**
55
     * @var Site|\TYPO3\CMS\Core\Site\Entity\Site
56
     */
57
    protected $site;
58

59
    public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager): void
60
    {
61
        $this->configurationManager = $configurationManager;
14✔
62
    }
63

64
    public function initializeArguments(): void
65
    {
66
        parent::initializeArguments();
7✔
67
        $this->registerUniversalTagAttributes();
7✔
68
        $this->registerArgument(
7✔
69
            'tagName',
7✔
70
            'string',
7✔
71
            'Tag name to use for enclosing container, list and flags (not finished) only',
7✔
72
            false,
7✔
73
            'ul'
7✔
74
        );
7✔
75
        $this->registerArgument(
7✔
76
            'tagNameChildren',
7✔
77
            'string',
7✔
78
            'Tag name to use for child nodes surrounding links, list and flags only',
7✔
79
            false,
7✔
80
            'li'
7✔
81
        );
7✔
82
        $this->registerArgument('defaultIsoFlag', 'string', 'ISO code of the default flag');
7✔
83
        $this->registerArgument('defaultLanguageLabel', 'string', 'Label for the default language');
7✔
84
        $this->registerArgument('order', 'mixed', 'Orders the languageIds after this list', false, '');
7✔
85
        $this->registerArgument('labelOverwrite', 'mixed', 'Overrides language labels');
7✔
86
        $this->registerArgument(
7✔
87
            'hideNotTranslated',
7✔
88
            'boolean',
7✔
89
            'Hides languageIDs which are not translated',
7✔
90
            false,
7✔
91
            false
7✔
92
        );
7✔
93
        $this->registerArgument(
7✔
94
            'layout',
7✔
95
            'string',
7✔
96
            'How to render links when using autorendering. Possible selections: name,flag - use fx "name" or ' .
7✔
97
            '"flag,name" or "name,flag"',
7✔
98
            false,
7✔
99
            'flag,name'
7✔
100
        );
7✔
101
        $this->registerArgument(
7✔
102
            'useCHash',
7✔
103
            'boolean',
7✔
104
            'Use cHash for typolink. Has no effect on TYPO3 v9.5+',
7✔
105
            false,
7✔
106
            true
7✔
107
        );
7✔
108
        $this->registerArgument('flagPath', 'string', 'Overwrites the path to the flag folder', false, '');
7✔
109
        $this->registerArgument('flagImageType', 'string', 'Sets type of flag image: png, gif, jpeg', false, 'svg');
7✔
110
        $this->registerArgument('linkCurrent', 'boolean', 'Sets flag to link current language or not', false, true);
7✔
111
        $this->registerArgument(
7✔
112
            'classCurrent',
7✔
113
            'string',
7✔
114
            'Sets the class, by which the current language will be marked',
7✔
115
            false,
7✔
116
            'current'
7✔
117
        );
7✔
118
        $this->registerArgument(
7✔
119
            'as',
7✔
120
            'string',
7✔
121
            'If used, stores the menu pages as an array in a variable named according to this value and renders ' .
7✔
122
            'the tag content - which means automatic rendering is disabled if this attribute is used',
7✔
123
            false,
7✔
124
            'languageMenu'
7✔
125
        );
7✔
126
        $this->registerArgument('pageUid', 'integer', 'Optional page uid to use.', false, 0);
7✔
127
        $this->registerArgument('configuration', 'array', 'Additional typoLink configuration', false, []);
7✔
128
        $this->registerArgument('excludeQueryVars', 'string', 'Comma-separate list of variables to exclude', false, '');
7✔
129
        $this->registerArgument(
7✔
130
            'languages',
7✔
131
            'mixed',
7✔
132
            'Array, CSV or Traversable containing UIDs of languages to render'
7✔
133
        );
7✔
134
    }
135

136
    public function render(): string
137
    {
UNCOV
138
        if (!is_object($GLOBALS['TSFE']->sys_page)) {
×
UNCOV
139
            return '';
×
140
        }
141
        /** @var ContentObjectRenderer|null $contentObject */
UNCOV
142
        $contentObject = ContentObjectFetcher::resolve($this->configurationManager);
×
143
        if ($contentObject === null) {
×
144
            throw new Exception('v:page.languageMenu requires a ContentObjectRenderer, none found', 1737807859);
×
145
        }
146

147
        $this->cObj = $contentObject;
×
148
        $this->tagName = is_scalar($this->arguments['tagName']) ? (string) $this->arguments['tagName'] : 'ul';
×
149
        $this->tag->setTagName($this->tagName);
×
UNCOV
150
        $this->site = $this->getSite();
×
UNCOV
151
        $this->defaultLangUid = $this->site->getDefaultLanguage()->getLanguageId();
×
152
        $this->languageMenu = $this->parseLanguageMenu();
×
153

154
        /** @var string $as */
155
        $as = $this->arguments['as'];
×
156
        $this->renderingContext->getVariableProvider()->add($as, $this->languageMenu);
×
157
        /** @var string|null $content */
UNCOV
158
        $content = $this->renderChildren();
×
UNCOV
159
        $content = is_scalar($content) ? (string) $content : '';
×
160
        $this->renderingContext->getVariableProvider()->remove($as);
×
161
        if (0 === mb_strlen(trim($content))) {
×
UNCOV
162
            $content = $this->autoRender();
×
163
        }
164
        return $content;
×
165
    }
166

167
    protected function autoRender(): string
168
    {
169
        $content = $this->getLanguageMenu();
×
UNCOV
170
        $content = trim($content);
×
UNCOV
171
        if (!empty($content)) {
×
UNCOV
172
            $this->tag->setContent($content);
×
UNCOV
173
            $content = $this->tag->render();
×
174
        }
175
        return $content;
×
176
    }
177

178
    /**
179
     * Get layout 0 (default): list
180
     */
181
    protected function getLanguageMenu(): string
182
    {
UNCOV
183
        $tagName = $this->arguments['tagNameChildren'];
×
UNCOV
184
        $html = [];
×
UNCOV
185
        $itemCount = count($this->languageMenu);
×
UNCOV
186
        foreach ($this->languageMenu as $index => $var) {
×
UNCOV
187
            $class = '';
×
188
            $classes = [];
×
189
            if ($var['inactive']) {
×
190
                $classes[] = 'inactive';
×
191
            }
192
            if ($var['current']) {
×
193
                $classes[] = $this->arguments['classCurrent'];
×
194
            }
195
            if (0 === $index) {
×
UNCOV
196
                $classes[] = 'first';
×
197
            } elseif (($itemCount - 1) === $index) {
×
198
                $classes[] = 'last';
×
199
            }
200
            if (0 < count($classes)) {
×
201
                $class = ' class="' . implode(' ', $classes) . '" ';
×
202
            }
203
            if ($var['current'] && !$this->arguments['linkCurrent']) {
×
UNCOV
204
                $html[] = '<' . $tagName . $class . '>' . $this->getLayout($var) . '</' . $tagName . '>';
×
205
            } else {
206
                $html[] = '<' . $tagName . $class . '><a href="' . htmlspecialchars($var['url']) . '">' .
×
UNCOV
207
                    $this->getLayout($var) . '</a></' . $tagName . '>';
×
208
            }
209
        }
UNCOV
210
        return implode(LF, $html);
×
211
    }
212

213
    /**
214
     * Returns the flag source given the language ISO code.
215
     */
216
    protected function getLanguageFlag(string $iso, string $label): string
217
    {
218
        /** @var string $flagPath */
UNCOV
219
        $flagPath = $this->arguments['flagPath'];
×
220
        /** @var string $flagImageType */
UNCOV
221
        $flagImageType = $this->arguments['flagImageType'];
×
UNCOV
222
        if ('' !== $flagPath) {
×
UNCOV
223
            $path = trim($flagPath);
×
224
        } else {
UNCOV
225
            $path = CoreUtility::getLanguageFlagIconPath();
×
226
        }
227

228
        $imgType = trim($flagImageType);
×
UNCOV
229
        $conf = [
×
230
            'file' => $path . strtoupper($iso) . '.' . $imgType,
×
UNCOV
231
            'altText' => $label,
×
UNCOV
232
            'titleText' => $label
×
233
        ];
×
234

235
        if (file_exists($conf['file'])) {
×
236
            $contentObjectDefinition = $this->cObj->getContentObject('IMAGE');
×
237
            if ($contentObjectDefinition === null) {
×
238
                return '';
×
239
            }
240
            return $this->cObj->render($contentObjectDefinition, $conf);
×
241
        }
242
        return '';
×
243
    }
244

245
    /**
246
     * Returns the flag source given a TYPO3 icon identifier.
247
     */
248
    protected function getLanguageFlagByIdentifier(string $identifier): string
249
    {
250
        /** @var IconFactory $iconFactory */
UNCOV
251
        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
×
UNCOV
252
        $icon = $iconFactory->getIcon($identifier, Icon::SIZE_SMALL);
×
UNCOV
253
        return $icon->render();
×
254
    }
255

256
    /**
257
     * Return the layout: flag & text, flags only or text only.
258
     */
259
    protected function getLayout(array $language): string
260
    {
261
        /** @var string $layout */
UNCOV
262
        $layout = $this->arguments['layout'];
×
263
        /** @var string $flagCode */
UNCOV
264
        $flagCode = $language['flagCode'];
×
UNCOV
265
        $flagImage = false !== stripos($layout, 'flag') ? $flagCode : '';
×
266
        /** @var string $label */
267
        $label = $language['label'];
×
UNCOV
268
        switch ($this->arguments['layout']) {
×
269
            case 'flag':
×
270
                $html = $flagImage;
×
UNCOV
271
                break;
×
272
            case 'name':
×
273
                $html = $label;
×
274
                break;
×
275
            case 'name,flag':
×
276
                $html = $label;
×
277
                if ('' !== $flagImage) {
×
278
                    $html .= '&nbsp;' . $flagImage;
×
279
                }
280
                break;
×
281
            case 'flag,name':
×
282
            default:
283
                if ('' !== $flagImage) {
×
UNCOV
284
                    $html = $flagImage . '&nbsp;' . $label;
×
285
                } else {
286
                    $html = $label;
×
287
                }
288
        }
289
        return $html;
×
290
    }
291

292
    /**
293
     * Sets all parameter for langMenu.
294
     */
295
    protected function parseLanguageMenu(): array
296
    {
297
        /** @var array $languages */
UNCOV
298
        $languages = $this->arguments['languages'];
×
299
        /** @var string|null $orderArgument */
UNCOV
300
        $orderArgument = $this->arguments['order'];
×
301
        /** @var iterable $order */
UNCOV
302
        $order = $orderArgument ? GeneralUtility::trimExplode(',', $orderArgument) : '';
×
303
        /** @var string $labelOverwrite */
UNCOV
304
        $labelOverwrite = $this->arguments['labelOverwrite'];
×
305
        if (!empty($labelOverwrite)) {
×
306
            /** @var array $labelOverwrite */
307
            $labelOverwrite = GeneralUtility::trimExplode(',', $labelOverwrite);
×
308
        }
309

310
        // first gather languages into this array so we can reorder it later
UNCOV
311
        $limitLanguages = static::arrayFromArrayOrTraversableOrCSVStatic($languages ?? []);
×
312
        $limitLanguages = array_filter($limitLanguages);
×
UNCOV
313
        $tempArray = $this->getLanguagesFromSiteConfiguration($limitLanguages);
×
314

315
        // reorder languageMenu
316
        $languageMenu = [];
×
317
        if (!empty($order)) {
×
318
            foreach ($order as $value) {
×
UNCOV
319
                if (isset($tempArray[$value])) {
×
UNCOV
320
                    $languageMenu[$value] = $tempArray[$value];
×
321
                }
322
            }
323
        } else {
324
            $languageMenu = $tempArray;
×
325
        }
326

327
        // overwrite of label
UNCOV
328
        if (!empty($labelOverwrite)) {
×
329
            $i = 0;
×
UNCOV
330
            foreach ($languageMenu as $key => $value) {
×
UNCOV
331
                $languageMenu[$key]['label'] = $labelOverwrite[$i];
×
UNCOV
332
                $i++;
×
333
            }
334
        }
335

336
        // get the languages actually available on this page
337
        $languageUids = $this->getSystemLanguageUids();
×
338

UNCOV
339
        if (class_exists(LanguageAspect::class)) {
×
340
            /** @var Context $context */
UNCOV
341
            $context = GeneralUtility::makeInstance(Context::class);
×
342
            /** @var LanguageAspect $languageAspect */
UNCOV
343
            $languageAspect = $context->getAspect('language');
×
344
            $languageUid = $languageAspect->getId();
×
345
        } else {
346
            $languageUid = $GLOBALS['TSFE']->sys_language_uid;
×
347
        }
348

349
        foreach ($languageMenu as $key => $value) {
×
UNCOV
350
            $current = $languageUid === (int) $key ? 1 : 0;
×
351
            $inactive = in_array($key, $languageUids) || (int) $key === $this->defaultLangUid ? 0 : 1;
×
UNCOV
352
            $url = $this->getLanguageUrl($key);
×
UNCOV
353
            if (empty($url)) {
×
354
                $url = GeneralUtility::getIndpEnv('REQUEST_URI');
×
355
            }
356
            $languageMenu[$key]['current'] = $current;
×
357
            $languageMenu[$key]['inactive'] = $inactive;
×
358
            $languageMenu[$key]['url'] = $url;
×
359
            $languageMenu[$key]['flagSrc'] = $this->getLanguageFlag($value['flag'] ?? $value['iso'], $value['label']);
×
360
            // if the user has set a flag path, always use that over the TYPO3 icon factory so the user
361
            // has the option to use custom flag images based on the ISO code of the language.
362
            // if the user has not set a flag path, prefer the TYPO3 icon factory when an icon
363
            // identifier is available (i.e., when using the site-based language lookup) .
364
            if (isset($value['flagIdentifier']) && empty($this->arguments['flagPath'])) {
×
UNCOV
365
                $languageMenu[$key]['flagCode'] = $this->getLanguageFlagByIdentifier($value['flagIdentifier']);
×
366
            } else {
UNCOV
367
                $languageMenu[$key]['flagCode'] = $this->getLanguageFlag(
×
UNCOV
368
                    $value['flag'] ?? $value['iso'],
×
369
                    $value['label']
×
370
                );
×
371
            }
372
            if ($this->arguments['hideNotTranslated'] && $inactive) {
×
373
                unset($languageMenu[$key]);
×
374
            }
375
        }
376

377
        return $languageMenu;
×
378
    }
379

380
    /**
381
     * Get the list of languages from the sys_language table.
382
     */
383
    protected function getLanguagesFromSysLanguage(array $limitLanguages): array
384
    {
385
        // add default language
UNCOV
386
        $result[0] = [
×
UNCOV
387
            'label' => $this->arguments['defaultLanguageLabel'] ?? 'English',
×
UNCOV
388
            'flag' => $this->arguments['defaultIsoFlag'] ?? 'gb'
×
UNCOV
389
        ];
×
390

391
        $select = 'uid,title,flag';
×
392
        $from = 'sys_language';
×
393

394
        if (!empty($limitLanguages)) {
×
UNCOV
395
            $sysLanguage = $GLOBALS['TSFE']->cObj->getRecords(
×
396
                $from,
×
397
                ['selectFields' => $select, 'pidInList' => 'root', 'uidInList' => implode(',', $limitLanguages)]
×
UNCOV
398
            );
×
399
        } else {
400
            $sysLanguage = $GLOBALS['TSFE']->cObj->getRecords(
×
401
                $from,
×
402
                ['selectFields' => $select, 'pidInList' => 'root']
×
403
            );
×
404
        }
405

406
        foreach ($sysLanguage as $value) {
×
407
            $result[$value['uid']] = [
×
408
                'label' => $value['title'],
×
UNCOV
409
                'flag' => $value['flag'],
×
UNCOV
410
            ];
×
411
        }
412

413
        return $result;
×
414
    }
415

416
    /**
417
     * Get the list of languages from the site configuration.
418
     */
419
    protected function getLanguagesFromSiteConfiguration(array $limitLanguages): array
420
    {
UNCOV
421
        $site = $this->getSite();
×
422
        // get only languages set as visible in frontend
UNCOV
423
        $languages = $site->getLanguages();
×
UNCOV
424
        $defaultLanguage = $site->getDefaultLanguage();
×
425

426
        $result = [];
×
UNCOV
427
        foreach ($languages as $language) {
×
428
            if (!empty($limitLanguages) && !in_array($language->getLanguageId(), $limitLanguages)) {
×
429
                continue;
×
430
            }
431
            $label = $language->getNavigationTitle();
×
432
            $flag = $language->getFlagIdentifier();
×
433
            if ($language->getLanguageId() == $defaultLanguage->getLanguageId()) {
×
434
                // override label/flag of default language if given
UNCOV
435
                $label = $this->arguments['defaultLanguageLabel'] ?? $label;
×
436
                $flag = $this->arguments['defaultIsoFlag'] ?? $flag;
×
437
            }
438
            if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '12.4', '>=')) {
×
UNCOV
439
                $isoCode = $language->getLocale()->getLanguageCode();
×
440
            } else {
441
                $isoCode = $language->getTwoLetterIsoCode();
×
442
            }
443
            $result[$language->getLanguageId()] = [
×
444
                'label' => $label,
×
UNCOV
445
                'iso' => $isoCode,
×
446
                'flagIdentifier' => $flag
×
UNCOV
447
            ];
×
448
        }
449

450
        return $result;
×
451
    }
452

453
    /**
454
     * Get link of language menu entry
455
     *
456
     * @param int|string $languageId
457
     */
458
    protected function getLanguageUrl($languageId): string
459
    {
460
        /** @var string $excludeVarsArgument */
UNCOV
461
        $excludeVarsArgument = $this->arguments['excludeQueryVars'];
×
UNCOV
462
        $excludedVars = trim((string) $excludeVarsArgument);
×
UNCOV
463
        $config = [
×
UNCOV
464
            'parameter' => $this->getPageUid(),
×
UNCOV
465
            'returnLast' => 'url',
×
466
            'additionalParams' => '&L=' . $languageId,
×
467
            'addQueryString' => 1,
×
468
            'addQueryString.' => [
×
469
                'method' => 'GET',
×
470
                'exclude' => 'id,L,cHash' . ($excludedVars ? ',' . $excludedVars : '')
×
471
            ]
×
472
        ];
×
473
        if (is_array($this->arguments['configuration'])) {
×
474
            $config = $this->mergeArrays($config, $this->arguments['configuration']);
×
475
        }
476
        return $this->cObj->typoLink('', $config);
×
477
    }
478

479
    /**
480
     * Get page via pageUid argument or current id
481
     */
482
    protected function getPageUid(): int
483
    {
484
        /** @var int $pageUid */
UNCOV
485
        $pageUid = $this->arguments['pageUid'];
×
UNCOV
486
        $pageUid = (int) $pageUid;
×
UNCOV
487
        if (0 === $pageUid) {
×
UNCOV
488
            $pageUid = $GLOBALS['TSFE']->id;
×
489
        }
490

491
        return (int) $pageUid;
×
492
    }
493

494
    /**
495
     * Find the site corresponding to the page that the menu is being rendered for
496
     *
497
     * @return Site|\TYPO3\CMS\Core\Site\Entity\Site
498
     */
499
    protected function getSite()
500
    {
501
        /** @var SiteFinderProxy $siteFinder */
UNCOV
502
        $siteFinder = GeneralUtility::makeInstance(SiteFinderProxy::class);
×
UNCOV
503
        return $siteFinder->getSiteByPageId($this->getPageUid());
×
504
    }
505

506
    /**
507
     * Fetches system languages available on the page depending on the TYPO3 version.
508
     *
509
     * @return int[]
510
     * @phpcsSuppress
511
     * @see https://docs.typo3.org/typo3cms/extensions/core/Changelog/9.0/Important-82445-MigratePagesLanguageOverlayIntoPages.html
512
     */
513
    protected function getSystemLanguageUids(): array
514
    {
UNCOV
515
        $table = 'pages';
×
UNCOV
516
        $parentField = 'l10n_parent';
×
517

518
        /** @var ConnectionPool $connectionPool */
UNCOV
519
        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
×
520
        $connection = $connectionPool->getConnectionForTable($table);
×
521
        $queryBuilder = $connection->createQueryBuilder();
×
UNCOV
522
        $queryBuilder->select('sys_language_uid')
×
UNCOV
523
            ->from($table)
×
524
            ->where(
×
525
                $queryBuilder->expr()->eq($parentField, $this->getPageUid())
×
526
            );
×
527
        $result = DoctrineQueryProxy::executeQueryOnQueryBuilder($queryBuilder);
×
528
        $rows = DoctrineQueryProxy::fetchAllAssociative($result);
×
529

530
        return array_column($rows, 'sys_language_uid');
×
531
    }
532
}
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