Completed
Pull Request — 3.x (#6198)
by
unknown
03:30
created

getValueFromFieldDescription()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 37
rs 8.7057
c 0
b 0
f 0
cc 6
nc 8
nop 3
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Sonata Project package.
7
 *
8
 * (c) Thomas Rabaix <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Sonata\AdminBundle\Twig\Extension;
15
16
use Doctrine\Common\Util\ClassUtils;
17
use Psr\Log\LoggerInterface;
18
use Sonata\AdminBundle\Admin\AdminInterface;
19
use Sonata\AdminBundle\Admin\FieldDescriptionInterface;
20
use Sonata\AdminBundle\Admin\Pool;
21
use Sonata\AdminBundle\Exception\NoValueException;
22
use Sonata\AdminBundle\Templating\TemplateRegistryInterface;
23
use Symfony\Component\DependencyInjection\ContainerInterface;
24
use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException;
25
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
26
use Symfony\Component\Security\Acl\Voter\FieldVote;
27
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
28
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
29
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslationInterface;
30
use Symfony\Contracts\Translation\TranslatorInterface;
31
use Twig\Environment;
32
use Twig\Error\LoaderError;
33
use Twig\Extension\AbstractExtension;
34
use Twig\Template;
35
use Twig\TemplateWrapper;
36
use Twig\TwigFilter;
37
use Twig\TwigFunction;
38
39
/**
40
 * @final since sonata-project/admin-bundle 3.52
41
 *
42
 * @author Thomas Rabaix <[email protected]>
43
 */
44
class SonataAdminExtension extends AbstractExtension
45
{
46
    // @todo: there are more locales which are not supported by moment and they need to be translated/normalized/canonicalized here
47
    public const MOMENT_UNSUPPORTED_LOCALES = [
48
        'de' => ['de', 'de-at'],
49
        'es' => ['es', 'es-do'],
50
        'nl' => ['nl', 'nl-be'],
51
        'fr' => ['fr', 'fr-ca', 'fr-ch'],
52
    ];
53
54
    /**
55
     * @var Pool
56
     */
57
    protected $pool;
58
59
    /**
60
     * @var LoggerInterface
61
     */
62
    protected $logger;
63
64
    /**
65
     * @var TranslatorInterface|null
66
     */
67
    protected $translator;
68
69
    /**
70
     * @var string[]
71
     */
72
    private $xEditableTypeMapping = [];
73
74
    /**
75
     * @var ContainerInterface
76
     */
77
    private $templateRegistries;
78
79
    /**
80
     * @var AuthorizationCheckerInterface
81
     */
82
    private $securityChecker;
83
84
    public function __construct(
85
        Pool $pool,
86
        ?LoggerInterface $logger = null,
87
        $translator = null,
88
        ?ContainerInterface $templateRegistries = null,
89
        ?AuthorizationCheckerInterface $securityChecker = null
90
    ) {
91
        // NEXT_MAJOR: make the translator parameter required, move TranslatorInterface check to method signature
92
        // and remove this block
93
94
        if (null === $translator) {
95
            @trigger_error(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
96
                'The $translator parameter will be required fields with the 4.0 release.',
97
                E_USER_DEPRECATED
98
            );
99
        } else {
100
            if (!$translator instanceof TranslatorInterface) {
101
                @trigger_error(sprintf(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
102
                    'The $translator parameter should be an instance of "%s" and will be mandatory in 4.0.',
103
                    TranslatorInterface::class
104
                ), E_USER_DEPRECATED);
105
            }
106
107
            if (!$translator instanceof TranslatorInterface && !$translator instanceof LegacyTranslationInterface) {
108
                throw new \TypeError(sprintf(
0 ignored issues
show
Unused Code introduced by
The call to TypeError::__construct() has too many arguments starting with sprintf('Argument 2 must...get_class($translator)).

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
109
                    'Argument 2 must be an instance of "%s" or preferably "%s", "%s given"',
110
                    TranslatorInterface::class,
111
                    LegacyTranslationInterface::class,
112
                    \get_class($translator)
113
                ));
114
            }
115
        }
116
117
        $this->pool = $pool;
118
        $this->logger = $logger;
119
        $this->translator = $translator;
120
        $this->templateRegistries = $templateRegistries;
121
        $this->securityChecker = $securityChecker;
122
    }
123
124
    public function getFilters()
125
    {
126
        return [
127
            new TwigFilter(
128
                'render_list_element',
129
                [$this, 'renderListElement'],
130
                [
131
                    'is_safe' => ['html'],
132
                    'needs_environment' => true,
133
                ]
134
            ),
135
            new TwigFilter(
136
                'render_view_element',
137
                [$this, 'renderViewElement'],
138
                [
139
                    'is_safe' => ['html'],
140
                    'needs_environment' => true,
141
                ]
142
            ),
143
            new TwigFilter(
144
                'render_view_element_compare',
145
                [$this, 'renderViewElementCompare'],
146
                [
147
                    'is_safe' => ['html'],
148
                    'needs_environment' => true,
149
                ]
150
            ),
151
            new TwigFilter(
152
                'render_relation_element',
153
                [$this, 'renderRelationElement']
154
            ),
155
            new TwigFilter(
156
                'sonata_urlsafeid',
157
                [$this, 'getUrlSafeIdentifier']
158
            ),
159
            new TwigFilter(
160
                'sonata_xeditable_type',
161
                [$this, 'getXEditableType']
162
            ),
163
            new TwigFilter(
164
                'sonata_xeditable_choices',
165
                [$this, 'getXEditableChoices']
166
            ),
167
        ];
168
    }
169
170
    public function getFunctions()
171
    {
172
        return [
173
            new TwigFunction('canonicalize_locale_for_moment', [$this, 'getCanonicalizedLocaleForMoment'], ['needs_context' => true]),
174
            new TwigFunction('canonicalize_locale_for_select2', [$this, 'getCanonicalizedLocaleForSelect2'], ['needs_context' => true]),
175
            new TwigFunction('is_granted_affirmative', [$this, 'isGrantedAffirmative']),
176
        ];
177
    }
178
179
    public function getName()
180
    {
181
        return 'sonata_admin';
182
    }
183
184
    /**
185
     * render a list element from the FieldDescription.
186
     *
187
     * @param object $object
188
     * @param array  $params
189
     *
190
     * @return string
191
     */
192
    public function renderListElement(
193
        Environment $environment,
194
        $object,
195
        FieldDescriptionInterface $fieldDescription,
196
        $params = []
197
    ) {
198
        $template = $this->getTemplate(
199
            $fieldDescription,
200
            // NEXT_MAJOR: Remove this line and use commented line below instead
201
            $fieldDescription->getAdmin()->getTemplate('base_list_field'),
0 ignored issues
show
Deprecated Code introduced by
The method Sonata\AdminBundle\Admin...nterface::getTemplate() has been deprecated with message: since sonata-project/admin-bundle 3.35. To be removed in 4.0. Use TemplateRegistry services instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
202
            //$this->getTemplateRegistry($fieldDescription->getAdmin()->getCode())->getTemplate('base_list_field'),
203
            $environment
204
        );
205
206
        return $this->render($fieldDescription, $template, array_merge($params, [
207
            'admin' => $fieldDescription->getAdmin(),
208
            'object' => $object,
209
            'value' => $this->getValueFromFieldDescription($object, $fieldDescription),
210
            'field_description' => $fieldDescription,
211
        ]), $environment);
212
    }
213
214
    /**
215
     * @deprecated since sonata-project/admin-bundle 3.33, to be removed in 4.0. Use render instead
216
     *
217
     * @return string
218
     */
219
    public function output(
220
        FieldDescriptionInterface $fieldDescription,
221
        Template $template,
222
        array $parameters,
223
        Environment $environment
224
    ) {
225
        return $this->render(
226
            $fieldDescription,
227
            new TemplateWrapper($environment, $template),
228
            $parameters,
229
            $environment
230
        );
231
    }
232
233
    /**
234
     * return the value related to FieldDescription, if the associated object does no
235
     * exists => a temporary one is created.
236
     *
237
     * NEXT_MAJOR: remove $params parameter and @throws \RuntimeException.
238
     *
239
     * @param object $object
240
     *
241
     * @throws \RuntimeException
242
     *
243
     * @return mixed
244
     */
245
    public function getValueFromFieldDescription(
246
        $object,
247
        FieldDescriptionInterface $fieldDescription,
248
        array $params = []
249
    ) {
250
        if (isset(\func_get_args()[2])) {
251
            @trigger_error(sprintf(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
252
                'Passing argument 3 to %s() is deprecated since sonata-project/admin-bundle 3.x'
253
                .' and will be ignored in version 4.0.',
254
                __METHOD__
255
            ), E_USER_DEPRECATED);
256
        }
257
258
        // NEXT_MAJOR: remove this check
259
        if (isset($params['loop']) && $object instanceof \ArrayAccess) {
260
            throw new \RuntimeException('remove the loop requirement');
261
        }
262
263
        $value = null;
264
265
        try {
266
            $value = $fieldDescription->getValue($object);
267
        } catch (NoValueException $e) {
268
            if ($fieldDescription->getAssociationAdmin()) {
269
                $value = $fieldDescription->getAssociationAdmin()->getNewInstance();
270
            } else {
271
                // NEXT_MAJOR: throw the NoValueException.
272
                @trigger_error(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
273
                    'Accessing a non existing value is deprecated'
274
                    .' since sonata-project/admin-bundle 3.67 and will throw an exception in 4.0.',
275
                    E_USER_DEPRECATED
276
                );
277
            }
278
        }
279
280
        return $value;
281
    }
282
283
    /**
284
     * render a view element.
285
     *
286
     * @param object $object
287
     *
288
     * @return string
289
     */
290
    public function renderViewElement(
291
        Environment $environment,
292
        FieldDescriptionInterface $fieldDescription,
293
        $object
294
    ) {
295
        $template = $this->getTemplate(
296
            $fieldDescription,
297
            '@SonataAdmin/CRUD/base_show_field.html.twig',
298
            $environment
299
        );
300
301
        try {
302
            $value = $fieldDescription->getValue($object);
303
        } catch (NoValueException $e) {
304
            // NEXT_MAJOR: Remove the try catch in order to throw the NoValueException.
305
            @trigger_error(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
306
                'Accessing a non existing value is deprecated'
307
                .' since sonata-project/admin-bundle 3.67 and will throw an exception in 4.0.',
308
                E_USER_DEPRECATED
309
            );
310
311
            $value = null;
312
        }
313
314
        return $this->render($fieldDescription, $template, [
315
            'field_description' => $fieldDescription,
316
            'object' => $object,
317
            'value' => $value,
318
            'admin' => $fieldDescription->getAdmin(),
319
        ], $environment);
320
    }
321
322
    /**
323
     * render a compared view element.
324
     *
325
     * @param mixed $baseObject
326
     * @param mixed $compareObject
327
     *
328
     * @return string
329
     */
330
    public function renderViewElementCompare(
331
        Environment $environment,
332
        FieldDescriptionInterface $fieldDescription,
333
        $baseObject,
334
        $compareObject
335
    ) {
336
        $template = $this->getTemplate(
337
            $fieldDescription,
338
            '@SonataAdmin/CRUD/base_show_field.html.twig',
339
            $environment
340
        );
341
342
        try {
343
            $baseValue = $fieldDescription->getValue($baseObject);
344
        } catch (NoValueException $e) {
345
            // NEXT_MAJOR: Remove the try catch in order to throw the NoValueException.
346
            @trigger_error(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
347
                'Accessing a non existing value is deprecated'
348
                .' since sonata-project/admin-bundle 3.67 and will throw an exception in 4.0.',
349
                E_USER_DEPRECATED
350
            );
351
352
            $baseValue = null;
353
        }
354
355
        try {
356
            $compareValue = $fieldDescription->getValue($compareObject);
357
        } catch (NoValueException $e) {
358
            // NEXT_MAJOR: Remove the try catch in order to throw the NoValueException.
359
            @trigger_error(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
360
                'Accessing a non existing value is deprecated'
361
                .' since sonata-project/admin-bundle 3.67 and will throw an exception in 4.0.',
362
                E_USER_DEPRECATED
363
            );
364
365
            $compareValue = null;
366
        }
367
368
        $baseValueOutput = $template->render([
369
            'admin' => $fieldDescription->getAdmin(),
370
            'field_description' => $fieldDescription,
371
            'value' => $baseValue,
372
            'object' => $baseObject,
373
        ]);
374
375
        $compareValueOutput = $template->render([
376
            'field_description' => $fieldDescription,
377
            'admin' => $fieldDescription->getAdmin(),
378
            'value' => $compareValue,
379
            'object' => $compareObject,
380
        ]);
381
382
        // Compare the rendered output of both objects by using the (possibly) overridden field block
383
        $isDiff = $baseValueOutput !== $compareValueOutput;
384
385
        return $this->render($fieldDescription, $template, [
386
            'field_description' => $fieldDescription,
387
            'value' => $baseValue,
388
            'value_compare' => $compareValue,
389
            'is_diff' => $isDiff,
390
            'admin' => $fieldDescription->getAdmin(),
391
            'object' => $baseObject,
392
        ], $environment);
393
    }
394
395
    /**
396
     * @param mixed $element
397
     *
398
     * @throws \RuntimeException
399
     *
400
     * @return mixed
401
     */
402
    public function renderRelationElement($element, FieldDescriptionInterface $fieldDescription)
403
    {
404
        if (!\is_object($element)) {
405
            return $element;
406
        }
407
408
        $propertyPath = $fieldDescription->getOption('associated_property');
409
410
        if (null === $propertyPath) {
411
            // For BC kept associated_tostring option behavior
412
            $method = $fieldDescription->getOption('associated_tostring');
413
414
            if ($method) {
415
                @trigger_error(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
416
                    'Option "associated_tostring" is deprecated since version 2.3 and will be removed in 4.0. Use "associated_property" instead.',
417
                    E_USER_DEPRECATED
418
                );
419
            } else {
420
                $method = '__toString';
421
            }
422
423
            if (!method_exists($element, $method)) {
424
                throw new \RuntimeException(sprintf(
425
                    'You must define an `associated_property` option or create a `%s::__toString` method'
426
                    .' to the field option %s from service %s is ',
427
                    \get_class($element),
428
                    $fieldDescription->getName(),
429
                    $fieldDescription->getAdmin()->getCode()
430
                ));
431
            }
432
433
            return $element->{$method}();
434
        }
435
436
        if (\is_callable($propertyPath)) {
437
            return $propertyPath($element);
438
        }
439
440
        return $this->pool->getPropertyAccessor()->getValue($element, $propertyPath);
441
    }
442
443
    /**
444
     * Get the identifiers as a string that is safe to use in a url.
445
     *
446
     * @param object $model
447
     *
448
     * @return string string representation of the id that is safe to use in a url
449
     */
450
    public function getUrlSafeIdentifier($model, ?AdminInterface $admin = null)
451
    {
452
        if (null === $admin) {
453
            $class = ClassUtils::getClass($model);
454
            if (!$this->pool->hasAdminByClass($class)) {
455
                throw new \InvalidArgumentException('You must pass an admin.');
456
            }
457
458
            $admin = $this->pool->getAdminByClass($class);
459
        }
460
461
        return $admin->getUrlSafeIdentifier($model);
462
    }
463
464
    /**
465
     * @param string[] $xEditableTypeMapping
466
     */
467
    public function setXEditableTypeMapping($xEditableTypeMapping)
468
    {
469
        $this->xEditableTypeMapping = $xEditableTypeMapping;
470
    }
471
472
    /**
473
     * @return string|bool
474
     */
475
    public function getXEditableType($type)
476
    {
477
        return isset($this->xEditableTypeMapping[$type]) ? $this->xEditableTypeMapping[$type] : false;
478
    }
479
480
    /**
481
     * Return xEditable choices based on the field description choices options & catalogue options.
482
     * With the following choice options:
483
     *     ['Status1' => 'Alias1', 'Status2' => 'Alias2']
484
     * The method will return:
485
     *     [['value' => 'Status1', 'text' => 'Alias1'], ['value' => 'Status2', 'text' => 'Alias2']].
486
     *
487
     * @return array
488
     */
489
    public function getXEditableChoices(FieldDescriptionInterface $fieldDescription)
490
    {
491
        $choices = $fieldDescription->getOption('choices', []);
492
        $catalogue = $fieldDescription->getOption('catalogue');
493
        $xEditableChoices = [];
494
        if (!empty($choices)) {
495
            reset($choices);
496
            $first = current($choices);
497
            // the choices are already in the right format
498
            if (\is_array($first) && \array_key_exists('value', $first) && \array_key_exists('text', $first)) {
499
                $xEditableChoices = $choices;
500
            } else {
501
                foreach ($choices as $value => $text) {
502
                    if ($catalogue) {
503
                        if (null !== $this->translator) {
504
                            $text = $this->translator->trans($text, [], $catalogue);
505
                        // NEXT_MAJOR: Remove this check
506
                        } elseif (method_exists($fieldDescription->getAdmin(), 'trans')) {
507
                            $text = $fieldDescription->getAdmin()->trans($text, [], $catalogue);
0 ignored issues
show
Deprecated Code introduced by
The method Sonata\AdminBundle\Admin\AdminInterface::trans() has been deprecated with message: since sonata-project/admin-bundle 3.9, to be removed in 4.0

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
508
                        }
509
                    }
510
511
                    $xEditableChoices[] = [
512
                        'value' => $value,
513
                        'text' => $text,
514
                    ];
515
                }
516
            }
517
        }
518
519
        if (false === $fieldDescription->getOption('required', true)
520
            && false === $fieldDescription->getOption('multiple', false)
521
        ) {
522
            $xEditableChoices = array_merge([[
523
                'value' => '',
524
                'text' => '',
525
            ]], $xEditableChoices);
526
        }
527
528
        return $xEditableChoices;
529
    }
530
531
    /**
532
     * Returns a canonicalized locale for "moment" NPM library,
533
     * or `null` if the locale's language is "en", which doesn't require localization.
534
     *
535
     * @return string|null
536
     */
537
    final public function getCanonicalizedLocaleForMoment(array $context)
538
    {
539
        $locale = strtolower(str_replace('_', '-', $context['app']->getRequest()->getLocale()));
540
541
        // "en" language doesn't require localization.
542
        if (('en' === $lang = substr($locale, 0, 2)) && !\in_array($locale, ['en-au', 'en-ca', 'en-gb', 'en-ie', 'en-nz'], true)) {
543
            return null;
544
        }
545
546
        foreach (self::MOMENT_UNSUPPORTED_LOCALES as $language => $locales) {
547
            if ($language === $lang && !\in_array($locale, $locales, true)) {
548
                $locale = $language;
549
            }
550
        }
551
552
        return $locale;
553
    }
554
555
    /**
556
     * Returns a canonicalized locale for "select2" NPM library,
557
     * or `null` if the locale's language is "en", which doesn't require localization.
558
     *
559
     * @return string|null
560
     */
561
    final public function getCanonicalizedLocaleForSelect2(array $context)
562
    {
563
        $locale = str_replace('_', '-', $context['app']->getRequest()->getLocale());
564
565
        // "en" language doesn't require localization.
566
        if ('en' === $lang = substr($locale, 0, 2)) {
567
            return null;
568
        }
569
570
        switch ($locale) {
571
            case 'pt':
572
                $locale = 'pt-PT';
573
                break;
574
            case 'ug':
575
                $locale = 'ug-CN';
576
                break;
577
            case 'zh':
578
                $locale = 'zh-CN';
579
                break;
580
            default:
581
                if (!\in_array($locale, ['pt-BR', 'pt-PT', 'ug-CN', 'zh-CN', 'zh-TW'], true)) {
582
                    $locale = $lang;
583
                }
584
        }
585
586
        return $locale;
587
    }
588
589
    /**
590
     * @param string|array $role
591
     * @param object|null  $object
592
     * @param string|null  $field
593
     *
594
     * @return bool
595
     */
596
    public function isGrantedAffirmative($role, $object = null, $field = null)
597
    {
598
        if (null === $this->securityChecker) {
599
            return false;
600
        }
601
602
        if (null !== $field) {
603
            $object = new FieldVote($object, $field);
604
        }
605
606
        if (!\is_array($role)) {
607
            $role = [$role];
608
        }
609
610
        foreach ($role as $oneRole) {
611
            try {
612
                if ($this->securityChecker->isGranted($oneRole, $object)) {
613
                    return true;
614
                }
615
            } catch (AuthenticationCredentialsNotFoundException $e) {
616
                // empty on purpose
617
            }
618
        }
619
620
        return false;
621
    }
622
623
    /**
624
     * Get template.
625
     *
626
     * @param string $defaultTemplate
627
     *
628
     * @return TemplateWrapper
629
     */
630
    protected function getTemplate(
631
        FieldDescriptionInterface $fieldDescription,
632
        $defaultTemplate,
633
        Environment $environment
634
    ) {
635
        $templateName = $fieldDescription->getTemplate() ?: $defaultTemplate;
636
637
        try {
638
            $template = $environment->load($templateName);
639
        } catch (LoaderError $e) {
640
            @trigger_error(sprintf(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
641
                'Relying on default template loading on field template loading exception is deprecated since 3.1'
642
                .' and will be removed in 4.0. A %s exception will be thrown instead',
643
                LoaderError::class
644
            ), E_USER_DEPRECATED);
645
            $template = $environment->load($defaultTemplate);
646
647
            if (null !== $this->logger) {
648
                $this->logger->warning(sprintf(
649
                    'An error occured trying to load the template "%s" for the field "%s",'
650
                    .' the default template "%s" was used instead.',
651
                    $templateName,
652
                    $fieldDescription->getFieldName(),
653
                    $defaultTemplate
654
                ), ['exception' => $e]);
655
            }
656
        }
657
658
        return $template;
659
    }
660
661
    private function render(
662
        FieldDescriptionInterface $fieldDescription,
663
        TemplateWrapper $template,
664
        array $parameters,
665
        Environment $environment
666
    ): ?string {
667
        $content = $template->render($parameters);
668
669
        if ($environment->isDebug()) {
670
            $commentTemplate = <<<'EOT'
671
672
<!-- START
673
    fieldName: %s
674
    template: %s
675
    compiled template: %s
676
    -->
677
    %s
678
<!-- END - fieldName: %s -->
679
EOT;
680
681
            return sprintf(
682
                $commentTemplate,
683
                $fieldDescription->getFieldName(),
684
                $fieldDescription->getTemplate(),
685
                $template->getSourceContext()->getName(),
686
                $content,
687
                $fieldDescription->getFieldName()
688
            );
689
        }
690
691
        return $content;
692
    }
693
694
    /**
695
     * @throws ServiceCircularReferenceException
696
     * @throws ServiceNotFoundException
697
     */
698
    private function getTemplateRegistry(string $adminCode): TemplateRegistryInterface
699
    {
700
        $serviceId = $adminCode.'.template_registry';
701
        $templateRegistry = $this->templateRegistries->get($serviceId);
702
703
        if ($templateRegistry instanceof TemplateRegistryInterface) {
704
            return $templateRegistry;
705
        }
706
707
        throw new ServiceNotFoundException($serviceId);
708
    }
709
}
710