Test Setup Failed
Pull Request — main (#426)
by MusikAnimal
17:10 queued 11:44
created

AppExtension::getLangName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file contains only the AppExtension class.
4
 */
5
6
declare(strict_types = 1);
7
8
namespace App\Twig;
9
10
use App\Helper\I18nHelper;
11
use App\Model\Edit;
12
use App\Model\Project;
13
use App\Model\User;
14
use App\Repository\ProjectRepository;
15
use DateTime;
16
use Symfony\Component\DependencyInjection\ContainerInterface;
17
use Symfony\Component\HttpFoundation\Request;
18
use Symfony\Component\HttpFoundation\RequestStack;
19
use Symfony\Component\HttpFoundation\Session\SessionInterface;
20
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
21
use Twig\Extension\AbstractExtension;
22
use Twig\TwigFilter;
23
use Twig\TwigFunction;
24
use Wikimedia\IPUtils;
25
26
/**
27
 * Twig functions and filters for XTools.
28
 */
29
class AppExtension extends AbstractExtension
30
{
31
    /** @var ContainerInterface The application's container interface. */
32
    protected $container;
33
34
    /** @var RequestStack The request stack. */
35
    protected $requestStack;
36
37
    /** @var SessionInterface User's current session. */
38
    protected $session;
39
40
    /** @var I18nHelper For i18n and l10n. */
41
    protected $i18n;
42
43
    /** @var float Duration of the current HTTP request in seconds. */
44
    protected $requestTime;
45
46
    /** @var UrlGeneratorInterface */
47
    private $urlGenerator;
48
49
    /**
50
     * Constructor, with the I18nHelper through dependency injection.
51
     * @param ContainerInterface $container
52
     * @param RequestStack $requestStack
53
     * @param SessionInterface $session
54
     * @param I18nHelper $i18n
55
     * @param UrlGeneratorInterface $generator
56
     */
57
    public function __construct(
58
        ContainerInterface $container,
59
        RequestStack $requestStack,
60
        SessionInterface $session,
61
        I18nHelper $i18n,
62
        UrlGeneratorInterface $generator
63
    ) {
64
        $this->container = $container;
65
        $this->requestStack = $requestStack;
66
        $this->session = $session;
67
        $this->i18n = $i18n;
68
        $this->urlGenerator = $generator;
69
    }
70
71
    /*********************************** FUNCTIONS ***********************************/
72
73
    /**
74
     * Get all functions that this class provides.
75
     * @return TwigFunction[]
76
     * @codeCoverageIgnore
77
     */
78
    public function getFunctions(): array
79
    {
80
        return [
81
            new TwigFunction('request_time', [$this, 'requestTime']),
82
            new TwigFunction('memory_usage', [$this, 'requestMemory']),
83
            new TwigFunction('msgIfExists', [$this, 'msgIfExists'], ['is_safe' => ['html']]),
84
            new TwigFunction('msgExists', [$this, 'msgExists'], ['is_safe' => ['html']]),
85
            new TwigFunction('msg', [$this, 'msg'], ['is_safe' => ['html']]),
86
            new TwigFunction('lang', [$this, 'getLang']),
87
            new TwigFunction('langName', [$this, 'getLangName']),
88
            new TwigFunction('fallbackLangs', [$this, 'getFallbackLangs']),
89
            new TwigFunction('allLangs', [$this, 'getAllLangs']),
90
            new TwigFunction('isRTL', [$this, 'isRTL']),
91
            new TwigFunction('shortHash', [$this, 'gitShortHash']),
92
            new TwigFunction('hash', [$this, 'gitHash']),
93
            new TwigFunction('releaseDate', [$this, 'gitDate']),
94
            new TwigFunction('enabled', [$this, 'toolEnabled']),
95
            new TwigFunction('tools', [$this, 'tools']),
96
            new TwigFunction('color', [$this, 'getColorList']),
97
            new TwigFunction('chartColor', [$this, 'chartColor']),
98
            new TwigFunction('isSingleWiki', [$this, 'isSingleWiki']),
99
            new TwigFunction('getReplagThreshold', [$this, 'getReplagThreshold']),
100
            new TwigFunction('isWMFLabs', [$this, 'isWMFLabs']),
101
            new TwigFunction('replag', [$this, 'replag']),
102
            new TwigFunction('quote', [$this, 'quote']),
103
            new TwigFunction('bugReportURL', [$this, 'bugReportURL']),
104
            new TwigFunction('logged_in_user', [$this, 'loggedInUser']),
105
            new TwigFunction('isUserAnon', [$this, 'isUserAnon']),
106
            new TwigFunction('nsName', [$this, 'nsName']),
107
            new TwigFunction('titleWithNs', [$this, 'titleWithNs']),
108
            new TwigFunction('formatDuration', [$this, 'formatDuration']),
109
            new TwigFunction('numberFormat', [$this, 'numberFormat']),
110
            new TwigFunction('buildQuery', [$this, 'buildQuery']),
111
            new TwigFunction('login_url', [$this, 'loginUrl']),
112
        ];
113
    }
114
115
    /**
116
     * Get the duration of the current HTTP request in seconds.
117
     * @return float
118
     * Untestable since there is no request stack in the tests.
119
     * @codeCoverageIgnore
120
     */
121
    public function requestTime(): float
122
    {
123
        if (!isset($this->requestTime)) {
124
            $this->requestTime = microtime(true) - $this->getRequest()->server->get('REQUEST_TIME_FLOAT');
125
        }
126
127
        return $this->requestTime;
128
    }
129
130
    /**
131
     * Get the formatted real memory usage.
132
     * @return float
133
     */
134
    public function requestMemory(): float
135
    {
136
        $mem = memory_get_usage(false);
137
        $div = pow(1024, 2);
138
        return $mem / $div;
139
    }
140
141
    /**
142
     * Get an i18n message.
143
     * @param string $message
144
     * @param string[] $vars
145
     * @return string|null
146
     */
147
    public function msg(string $message = '', array $vars = []): ?string
148
    {
149
        return $this->i18n->msg($message, $vars);
150
    }
151
152
    /**
153
     * See if a given i18n message exists.
154
     * @param string $message The message.
155
     * @param string[] $vars
156
     * @return bool
157
     */
158
    public function msgExists(?string $message, array $vars = []): bool
159
    {
160
        return $this->i18n->msgExists($message, $vars);
161
    }
162
163
    /**
164
     * Get an i18n message if it exists, otherwise just get the message key.
165
     * @param string $message
166
     * @param string[] $vars
167
     * @return string
168
     */
169
    public function msgIfExists(?string $message, array $vars = []): string
170
    {
171
        return $this->i18n->msgIfExists($message, $vars);
172
    }
173
174
    /**
175
     * Get the current language code.
176
     * @return string
177
     */
178
    public function getLang(): string
179
    {
180
        return $this->i18n->getLang();
181
    }
182
183
    /**
184
     * Get the current language name (defaults to 'English').
185
     * @return string
186
     */
187
    public function getLangName(): string
188
    {
189
        return $this->i18n->getLangName();
190
    }
191
192
    /**
193
     * Get the fallback languages for the current language, so we know what to load with jQuery.i18n.
194
     * @return string[]
195
     */
196
    public function getFallbackLangs(): array
197
    {
198
        return $this->i18n->getFallbacks();
199
    }
200
201
    /**
202
     * Get all available languages in the i18n directory
203
     * @return string[] Associative array of langKey => langName
204
     */
205
    public function getAllLangs(): array
206
    {
207
        return $this->i18n->getAllLangs();
208
    }
209
210
    /**
211
     * Whether the current language is right-to-left.
212
     * @param string|null $lang Optionally provide a specific lanuage code.
213
     * @return bool
214
     */
215
    public function isRTL(?string $lang = null): bool
216
    {
217
        return $this->i18n->isRTL($lang);
218
    }
219
220
    /**
221
     * Get the short hash of the currently checked-out Git commit.
222
     * @return string
223
     */
224
    public function gitShortHash(): string
225
    {
226
        return exec('git rev-parse --short HEAD');
227
    }
228
229
    /**
230
     * Get the full hash of the currently checkout-out Git commit.
231
     * @return string
232
     */
233
    public function gitHash(): string
234
    {
235
        return exec('git rev-parse HEAD');
236
    }
237
238
    /**
239
     * Get the date of the HEAD commit.
240
     * @return string
241
     */
242
    public function gitDate(): string
243
    {
244
        $date = new DateTime(exec('git show -s --format=%ci'));
245
        return $this->dateFormat($date, 'yyyy-MM-dd');
246
    }
247
248
    /**
249
     * Check whether a given tool is enabled.
250
     * @param string $tool The short name of the tool.
251
     * @return bool
252
     */
253
    public function toolEnabled(string $tool = 'index'): bool
254
    {
255
        $param = false;
256
        if ($this->container->hasParameter("enable.$tool")) {
257
            $param = boolval($this->container->getParameter("enable.$tool"));
258
        }
259
        return $param;
260
    }
261
262
    /**
263
     * Get a list of the short names of all tools.
264
     * @return string[]
265
     */
266
    public function tools(): array
267
    {
268
        $retVal = [];
269
        if ($this->container->hasParameter('tools')) {
270
            $retVal = $this->container->getParameter('tools');
271
        }
272
        return $retVal;
273
    }
274
275
    /**
276
     * Get a list of namespace colours (one or all).
277
     * @param int|false $num The NS ID to get. False to get the full list.
278
     * @return string|string[] Color or all all colors indexed by namespace ID.
279
     */
280
    public static function getColorList($num = false)
281
    {
282
        $colors = [
283
            0 => '#FF5555',
284
            1 => '#55FF55',
285
            2 => '#FFEE22',
286
            3 => '#FF55FF',
287
            4 => '#5555FF',
288
            5 => '#55FFFF',
289
            6 => '#C00000',
290
            7 => '#0000C0',
291
            8 => '#008800',
292
            9 => '#00C0C0',
293
            10 => '#FFAFAF',
294
            11 => '#808080',
295
            12 => '#00C000',
296
            13 => '#404040',
297
            14 => '#C0C000',
298
            15 => '#C000C0',
299
            90 => '#991100',
300
            91 => '#99FF00',
301
            92 => '#000000',
302
            93 => '#777777',
303
            100 => '#75A3D1',
304
            101 => '#A679D2',
305
            102 => '#660000',
306
            103 => '#000066',
307
            104 => '#FAFFAF',
308
            105 => '#408345',
309
            106 => '#5c8d20',
310
            107 => '#e1711d',
311
            108 => '#94ef2b',
312
            109 => '#756a4a',
313
            110 => '#6f1dab',
314
            111 => '#301e30',
315
            112 => '#5c9d96',
316
            113 => '#a8cd8c',
317
            114 => '#f2b3f1',
318
            115 => '#9b5828',
319
            116 => '#002288',
320
            117 => '#0000CC',
321
            118 => '#99FFFF',
322
            119 => '#99BBFF',
323
            120 => '#FF99FF',
324
            121 => '#CCFFFF',
325
            122 => '#CCFF00',
326
            123 => '#CCFFCC',
327
            200 => '#33FF00',
328
            201 => '#669900',
329
            202 => '#666666',
330
            203 => '#999999',
331
            204 => '#FFFFCC',
332
            205 => '#FF00CC',
333
            206 => '#FFFF00',
334
            207 => '#FFCC00',
335
            208 => '#FF0000',
336
            209 => '#FF6600',
337
            250 => '#6633CC',
338
            251 => '#6611AA',
339
            252 => '#66FF99',
340
            253 => '#66FF66',
341
            446 => '#06DCFB',
342
            447 => '#892EE4',
343
            460 => '#99FF66',
344
            461 => '#99CC66',
345
            470 => '#CCCC33',
346
            471 => '#CCFF33',
347
            480 => '#6699FF',
348
            481 => '#66FFFF',
349
            484 => '#07C8D6',
350
            485 => '#2AF1FF',
351
            486 => '#79CB21',
352
            487 => '#80D822',
353
            490 => '#995500',
354
            491 => '#998800',
355
            710 => '#FFCECE',
356
            711 => '#FFC8F2',
357
            828 => '#F7DE00',
358
            829 => '#BABA21',
359
            866 => '#FFFFFF',
360
            867 => '#FFCCFF',
361
            1198 => '#FF34B3',
362
            1199 => '#8B1C62',
363
            2300 => '#A900B8',
364
            2301 => '#C93ED6',
365
            2302 => '#8A09C1',
366
            2303 => '#974AB8',
367
            2600 => '#000000',
368
        ];
369
370
        if (false === $num) {
371
            return $colors;
372
        } elseif (isset($colors[$num])) {
373
            return $colors[$num];
374
        } else {
375
            // Default to grey.
376
            return '#CCC';
377
        }
378
    }
379
380
    /**
381
     * Get color-blind friendly colors for use in charts
382
     * @param int $num Index of color
383
     * @return string RGBA color (so you can more easily adjust the opacity)
384
     */
385
    public function chartColor(int $num): string
386
    {
387
        $colors = [
388
            'rgba(171, 212, 235, 1)',
389
            'rgba(178, 223, 138, 1)',
390
            'rgba(251, 154, 153, 1)',
391
            'rgba(253, 191, 111, 1)',
392
            'rgba(202, 178, 214, 1)',
393
            'rgba(207, 182, 128, 1)',
394
            'rgba(141, 211, 199, 1)',
395
            'rgba(252, 205, 229, 1)',
396
            'rgba(255, 247, 161, 1)',
397
            'rgba(252, 146, 114, 1)',
398
            'rgba(217, 217, 217, 1)',
399
        ];
400
401
        return $colors[$num % count($colors)];
402
    }
403
404
    /**
405
     * Whether XTools is running in single-project mode.
406
     * @return bool
407
     */
408
    public function isSingleWiki(): bool
409
    {
410
        $param = true;
411
        if ($this->container->hasParameter('app.single_wiki')) {
412
            $param = boolval($this->container->getParameter('app.single_wiki'));
413
        }
414
        return $param;
415
    }
416
417
    /**
418
     * Get the database replication-lag threshold.
419
     * @return int
420
     */
421
    public function getReplagThreshold(): int
422
    {
423
        $param = 30;
424
        if ($this->container->hasParameter('app.replag_threshold')) {
425
            $param = $this->container->getParameter('app.replag_threshold');
426
        }
427
        return $param;
428
    }
429
430
    /**
431
     * Whether XTools is running in WMF Labs mode.
432
     * @return bool
433
     */
434
    public function isWMFLabs(): bool
435
    {
436
        $param = false;
437
        if ($this->container->hasParameter('app.is_labs')) {
438
            $param = boolval($this->container->getParameter('app.is_labs'));
439
        }
440
        return $param;
441
    }
442
443
    /**
444
     * The current replication lag.
445
     * @return int
446
     * @codeCoverageIgnore
447
     */
448
    public function replag(): int
449
    {
450
        $projectIdent = $this->getRequest()->get('project', 'enwiki');
451
        $project = ProjectRepository::getProject($projectIdent, $this->container);
452
        $dbName = $project->getDatabaseName();
453
        $sql = "SELECT lag FROM `heartbeat_p`.`heartbeat`";
454
        return (int)$project->getRepository()->executeProjectsQuery($project, $sql, [
455
            'project' => $dbName,
456
        ])->fetchOne();
457
    }
458
459
    /**
460
     * Get a random quote for the footer
461
     * @return string
462
     */
463
    public function quote(): string
464
    {
465
        // Don't show if Quote is turned off, but always show for Labs
466
        // (so quote is in footer but not in nav).
467
        $isLabs = $this->container->getParameter('app.is_labs');
468
        if (!$isLabs && !$this->container->getParameter('enable.Quote')) {
469
            return '';
470
        }
471
        $quotes = $this->container->getParameter('quotes');
472
        $id = array_rand($quotes);
473
        return $quotes[$id];
474
    }
475
476
    /**
477
     * Get the currently logged in user's details.
478
     * @return string[]|object|null
479
     */
480
    public function loggedInUser()
481
    {
482
        return $this->container->get('session')->get('logged_in_user');
483
    }
484
485
    /**
486
     * Get a URL to the login route with parameters to redirect back to the current page after logging in.
487
     * @param Request $request
488
     * @return string
489
     */
490
    public function loginUrl(Request $request): string
491
    {
492
        return $this->urlGenerator->generate('login', [
493
            'callback' => $this->urlGenerator->generate(
494
                'oauth_callback',
495
                ['redirect' => $request->getUri()],
496
                UrlGeneratorInterface::ABSOLUTE_URL
497
            ),
498
        ], UrlGeneratorInterface::ABSOLUTE_URL);
499
    }
500
501
    /*********************************** FILTERS ***********************************/
502
503
    /**
504
     * Get all filters for this extension.
505
     * @return TwigFilter[]
506
     * @codeCoverageIgnore
507
     */
508
    public function getFilters(): array
509
    {
510
        return [
511
            new TwigFilter('ucfirst', [$this, 'capitalizeFirst']),
512
            new TwigFilter('percent_format', [$this, 'percentFormat']),
513
            new TwigFilter('diff_format', [$this, 'diffFormat'], ['is_safe' => ['html']]),
514
            new TwigFilter('num_format', [$this, 'numberFormat']),
515
            new TwigFilter('size_format', [$this, 'sizeFormat']),
516
            new TwigFilter('date_format', [$this, 'dateFormat']),
517
            new TwigFilter('wikify', [$this, 'wikify']),
518
        ];
519
    }
520
521
    /**
522
     * Format a number based on language settings.
523
     * @param int|float $number
524
     * @param int $decimals Number of decimals to format to.
525
     * @return string
526
     */
527
    public function numberFormat($number, int $decimals = 0): string
528
    {
529
        return $this->i18n->numberFormat($number, $decimals);
530
    }
531
532
    /**
533
     * Format the given size (in bytes) as KB, MB, GB, or TB.
534
     * Some code courtesy of Leo, CC BY-SA 4.0
535
     * @see https://stackoverflow.com/a/2510459/604142
536
     * @param int $bytes
537
     * @param int $precision
538
     * @return string
539
     */
540
    public function sizeFormat(int $bytes, int $precision = 2): string
541
    {
542
        $base = log($bytes, 1024);
543
        $suffixes = ['', 'kilobytes', 'megabytes', 'gigabytes', 'terabytes'];
544
545
        $index = floor($base);
546
547
        if (0 === (int)$index) {
548
            return $this->numberFormat($bytes);
549
        }
550
551
        $sizeMessage = $this->numberFormat(
552
            pow(1024, $base - floor($base)),
553
            $precision
554
        );
555
556
        return $this->i18n->msg('size-'.$suffixes[floor($base)], [$sizeMessage]);
557
    }
558
559
    /**
560
     * Localize the given date based on language settings.
561
     * @param string|int|DateTime $datetime
562
     * @param string $pattern Format according to this ICU date format.
563
     * @see http://userguide.icu-project.org/formatparse/datetime
564
     * @return string
565
     */
566
    public function dateFormat($datetime, string $pattern = 'yyyy-MM-dd HH:mm'): string
567
    {
568
        return $this->i18n->dateFormat($datetime, $pattern);
569
    }
570
571
    /**
572
     * Convert raw wikitext to HTML-formatted string.
573
     * @param string $str
574
     * @param Project $project
575
     * @return string
576
     */
577
    public function wikify(string $str, Project $project): string
578
    {
579
        return Edit::wikifyString($str, $project);
580
    }
581
582
    /**
583
     * Mysteriously missing Twig helper to capitalize only the first character.
584
     * E.g. used for table headings for translated messages
585
     * @param string $str The string
586
     * @return string The string, capitalized
587
     */
588
    public function capitalizeFirst(string $str): string
589
    {
590
        return ucfirst($str);
591
    }
592
593
    /**
594
     * Format a given number or fraction as a percentage.
595
     * @param int|float $numerator Numerator or single fraction if denominator is ommitted.
596
     * @param int $denominator Denominator.
597
     * @param integer $precision Number of decimal places to show.
598
     * @return string Formatted percentage.
599
     */
600
    public function percentFormat($numerator, ?int $denominator = null, int $precision = 1): string
601
    {
602
        return $this->i18n->percentFormat($numerator, $denominator, $precision);
603
    }
604
605
    /**
606
     * Helper to return whether the given user is an anonymous (logged out) user.
607
     * @param User|string $user User object or username as a string.
608
     * @return bool
609
     */
610
    public function isUserAnon($user): bool
611
    {
612
        if ($user instanceof User) {
613
            $username = $user->getUsername();
614
        } else {
615
            $username = $user;
616
        }
617
618
        return IPUtils::isIPAddress($username);
619
    }
620
621
    /**
622
     * Helper to properly translate a namespace name.
623
     * @param int|string $namespace Namespace key as a string or ID.
624
     * @param string[] $namespaces List of available namespaces as retrieved from Project::getNamespaces().
625
     * @return string Namespace name
626
     */
627
    public function nsName($namespace, array $namespaces): string
628
    {
629
        if ('all' === $namespace) {
630
            return $this->i18n->msg('all');
631
        } elseif ('0' === $namespace || 0 === $namespace || 'Main' === $namespace) {
632
            return $this->i18n->msg('mainspace');
633
        } else {
634
            return $namespaces[$namespace] ?? $this->i18n->msg('unknown');
635
        }
636
    }
637
638
    /**
639
     * Given a page title and namespace, generate the full page title.
640
     * @param string $title
641
     * @param int $namespace
642
     * @param array $namespaces
643
     * @return string
644
     */
645
    public function titleWithNs(string $title, int $namespace, array $namespaces): string
646
    {
647
        if (0 === $namespace) {
648
            return $title;
649
        }
650
        return $this->nsName($namespace, $namespaces).':'.$title;
651
    }
652
653
    /**
654
     * Format a given number as a diff, colouring it green if it's positive, red if negative, gary if zero
655
     * @param int $size Diff size
656
     * @return string Markup with formatted number
657
     */
658
    public function diffFormat(int $size): string
659
    {
660
        if ($size < 0) {
661
            $class = 'diff-neg';
662
        } elseif ($size > 0) {
663
            $class = 'diff-pos';
664
        } else {
665
            $class = 'diff-zero';
666
        }
667
668
        $size = $this->numberFormat($size);
669
670
        return "<span class='$class'".
671
            ($this->i18n->isRTL() ? " dir='rtl'" : '').
672
            ">$size</span>";
673
    }
674
675
    /**
676
     * Format a time duration as humanized string.
677
     * @param int $seconds Number of seconds.
678
     * @param bool $translate Used for unit testing. Set to false to return
679
     *   the value and i18n key, instead of the actual translation.
680
     * @return string|mixed[] Examples: '30 seconds', '2 minutes', '15 hours', '500 days',
681
     *   or [30, 'num-seconds'] (etc.) if $translate is false.
682
     */
683
    public function formatDuration(int $seconds, bool $translate = true)
684
    {
685
        [$val, $key] = $this->getDurationMessageKey($seconds);
686
687
        if ($translate) {
688
            return $this->numberFormat($val).' '.$this->i18n->msg("num-$key", [$val]);
689
        } else {
690
            return [$this->numberFormat($val), "num-$key"];
691
        }
692
    }
693
694
    /**
695
     * Given a time duration in seconds, generate a i18n message key and value.
696
     * @param int $seconds Number of seconds.
697
     * @return array<integer|string> [int - message value, string - message key]
698
     */
699
    private function getDurationMessageKey(int $seconds)
700
    {
701
        /** @var int $val Value to show in message */
702
        $val = $seconds;
703
704
        /** @var string $key Unit of time, used in the key for the i18n message */
705
        $key = 'seconds';
706
707
        if ($seconds >= 86400) {
708
            // Over a day
709
            $val = (int) floor($seconds / 86400);
710
            $key = 'days';
711
        } elseif ($seconds >= 3600) {
712
            // Over an hour, less than a day
713
            $val = (int) floor($seconds / 3600);
714
            $key = 'hours';
715
        } elseif ($seconds >= 60) {
716
            // Over a minute, less than an hour
717
            $val = (int) floor($seconds / 60);
718
            $key = 'minutes';
719
        }
720
721
        return [$val, $key];
722
    }
723
724
    /**
725
     * Build URL query string from given params.
726
     * @param string[] $params
727
     * @return string
728
     */
729
    public function buildQuery(array $params): string
730
    {
731
        return is_array($params) ? http_build_query($params) : '';
0 ignored issues
show
introduced by
The condition is_array($params) is always true.
Loading history...
732
    }
733
734
    /**
735
     * Shorthand to get the current request from the request stack.
736
     * @return \Symfony\Component\HttpFoundation\Request
737
     * There is no request stack in the tests.
738
     * @codeCoverageIgnore
739
     */
740
    private function getRequest(): \Symfony\Component\HttpFoundation\Request
741
    {
742
        return $this->container->get('request_stack')->getCurrentRequest();
743
    }
744
}
745