Completed
Pull Request — 3.x (#6198)
by
unknown
04:00
created

getValueFromFieldDescription()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 33
rs 8.7697
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.
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('Passing a third parameter is deprecated since sonata-project/admin-bundle 3.x and will be ignored in 4.0.', E_USER_DEPRECATED);
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
        }
253
254
        // NEXT_MAJOR: remove this check
255
        if (isset($params['loop']) && $object instanceof \ArrayAccess) {
256
            throw new \RuntimeException('remove the loop requirement');
257
        }
258
259
        $value = null;
260
261
        try {
262
            $value = $fieldDescription->getValue($object);
263
        } catch (NoValueException $e) {
264
            if ($fieldDescription->getAssociationAdmin()) {
265
                $value = $fieldDescription->getAssociationAdmin()->getNewInstance();
266
            } else {
267
                // NEXT_MAJOR: throw the NoValueException.
268
                @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...
269
                    'Accessing a non existing value is deprecated'
270
                    .' since sonata-project/admin-bundle 3.67 and will throw an exception in 4.0.',
271
                    E_USER_DEPRECATED
272
                );
273
            }
274
        }
275
276
        return $value;
277
    }
278
279
    /**
280
     * render a view element.
281
     *
282
     * @param object $object
283
     *
284
     * @return string
285
     */
286
    public function renderViewElement(
287
        Environment $environment,
288
        FieldDescriptionInterface $fieldDescription,
289
        $object
290
    ) {
291
        $template = $this->getTemplate(
292
            $fieldDescription,
293
            '@SonataAdmin/CRUD/base_show_field.html.twig',
294
            $environment
295
        );
296
297
        try {
298
            $value = $fieldDescription->getValue($object);
299
        } catch (NoValueException $e) {
300
            // NEXT_MAJOR: Remove the try catch in order to throw the NoValueException.
301
            @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...
302
                'Accessing a non existing value is deprecated'
303
                .' since sonata-project/admin-bundle 3.67 and will throw an exception in 4.0.',
304
                E_USER_DEPRECATED
305
            );
306
307
            $value = null;
308
        }
309
310
        return $this->render($fieldDescription, $template, [
311
            'field_description' => $fieldDescription,
312
            'object' => $object,
313
            'value' => $value,
314
            'admin' => $fieldDescription->getAdmin(),
315
        ], $environment);
316
    }
317
318
    /**
319
     * render a compared view element.
320
     *
321
     * @param mixed $baseObject
322
     * @param mixed $compareObject
323
     *
324
     * @return string
325
     */
326
    public function renderViewElementCompare(
327
        Environment $environment,
328
        FieldDescriptionInterface $fieldDescription,
329
        $baseObject,
330
        $compareObject
331
    ) {
332
        $template = $this->getTemplate(
333
            $fieldDescription,
334
            '@SonataAdmin/CRUD/base_show_field.html.twig',
335
            $environment
336
        );
337
338
        try {
339
            $baseValue = $fieldDescription->getValue($baseObject);
340
        } catch (NoValueException $e) {
341
            // NEXT_MAJOR: Remove the try catch in order to throw the NoValueException.
342
            @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...
343
                'Accessing a non existing value is deprecated'
344
                .' since sonata-project/admin-bundle 3.67 and will throw an exception in 4.0.',
345
                E_USER_DEPRECATED
346
            );
347
348
            $baseValue = null;
349
        }
350
351
        try {
352
            $compareValue = $fieldDescription->getValue($compareObject);
353
        } catch (NoValueException $e) {
354
            // NEXT_MAJOR: Remove the try catch in order to throw the NoValueException.
355
            @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...
356
                'Accessing a non existing value is deprecated'
357
                .' since sonata-project/admin-bundle 3.67 and will throw an exception in 4.0.',
358
                E_USER_DEPRECATED
359
            );
360
361
            $compareValue = null;
362
        }
363
364
        $baseValueOutput = $template->render([
365
            'admin' => $fieldDescription->getAdmin(),
366
            'field_description' => $fieldDescription,
367
            'value' => $baseValue,
368
            'object' => $baseObject,
369
        ]);
370
371
        $compareValueOutput = $template->render([
372
            'field_description' => $fieldDescription,
373
            'admin' => $fieldDescription->getAdmin(),
374
            'value' => $compareValue,
375
            'object' => $compareObject,
376
        ]);
377
378
        // Compare the rendered output of both objects by using the (possibly) overridden field block
379
        $isDiff = $baseValueOutput !== $compareValueOutput;
380
381
        return $this->render($fieldDescription, $template, [
382
            'field_description' => $fieldDescription,
383
            'value' => $baseValue,
384
            'value_compare' => $compareValue,
385
            'is_diff' => $isDiff,
386
            'admin' => $fieldDescription->getAdmin(),
387
            'object' => $baseObject,
388
        ], $environment);
389
    }
390
391
    /**
392
     * @param mixed $element
393
     *
394
     * @throws \RuntimeException
395
     *
396
     * @return mixed
397
     */
398
    public function renderRelationElement($element, FieldDescriptionInterface $fieldDescription)
399
    {
400
        if (!\is_object($element)) {
401
            return $element;
402
        }
403
404
        $propertyPath = $fieldDescription->getOption('associated_property');
405
406
        if (null === $propertyPath) {
407
            // For BC kept associated_tostring option behavior
408
            $method = $fieldDescription->getOption('associated_tostring');
409
410
            if ($method) {
411
                @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...
412
                    'Option "associated_tostring" is deprecated since version 2.3 and will be removed in 4.0. Use "associated_property" instead.',
413
                    E_USER_DEPRECATED
414
                );
415
            } else {
416
                $method = '__toString';
417
            }
418
419
            if (!method_exists($element, $method)) {
420
                throw new \RuntimeException(sprintf(
421
                    'You must define an `associated_property` option or create a `%s::__toString` method'
422
                    .' to the field option %s from service %s is ',
423
                    \get_class($element),
424
                    $fieldDescription->getName(),
425
                    $fieldDescription->getAdmin()->getCode()
426
                ));
427
            }
428
429
            return $element->{$method}();
430
        }
431
432
        if (\is_callable($propertyPath)) {
433
            return $propertyPath($element);
434
        }
435
436
        return $this->pool->getPropertyAccessor()->getValue($element, $propertyPath);
437
    }
438
439
    /**
440
     * Get the identifiers as a string that is safe to use in a url.
441
     *
442
     * @param object $model
443
     *
444
     * @return string string representation of the id that is safe to use in a url
445
     */
446
    public function getUrlSafeIdentifier($model, ?AdminInterface $admin = null)
447
    {
448
        if (null === $admin) {
449
            $class = ClassUtils::getClass($model);
450
            if (!$this->pool->hasAdminByClass($class)) {
451
                throw new \InvalidArgumentException('You must pass an admin.');
452
            }
453
454
            $admin = $this->pool->getAdminByClass($class);
455
        }
456
457
        return $admin->getUrlSafeIdentifier($model);
458
    }
459
460
    /**
461
     * @param string[] $xEditableTypeMapping
462
     */
463
    public function setXEditableTypeMapping($xEditableTypeMapping)
464
    {
465
        $this->xEditableTypeMapping = $xEditableTypeMapping;
466
    }
467
468
    /**
469
     * @return string|bool
470
     */
471
    public function getXEditableType($type)
472
    {
473
        return isset($this->xEditableTypeMapping[$type]) ? $this->xEditableTypeMapping[$type] : false;
474
    }
475
476
    /**
477
     * Return xEditable choices based on the field description choices options & catalogue options.
478
     * With the following choice options:
479
     *     ['Status1' => 'Alias1', 'Status2' => 'Alias2']
480
     * The method will return:
481
     *     [['value' => 'Status1', 'text' => 'Alias1'], ['value' => 'Status2', 'text' => 'Alias2']].
482
     *
483
     * @return array
484
     */
485
    public function getXEditableChoices(FieldDescriptionInterface $fieldDescription)
486
    {
487
        $choices = $fieldDescription->getOption('choices', []);
488
        $catalogue = $fieldDescription->getOption('catalogue');
489
        $xEditableChoices = [];
490
        if (!empty($choices)) {
491
            reset($choices);
492
            $first = current($choices);
493
            // the choices are already in the right format
494
            if (\is_array($first) && \array_key_exists('value', $first) && \array_key_exists('text', $first)) {
495
                $xEditableChoices = $choices;
496
            } else {
497
                foreach ($choices as $value => $text) {
498
                    if ($catalogue) {
499
                        if (null !== $this->translator) {
500
                            $text = $this->translator->trans($text, [], $catalogue);
501
                        // NEXT_MAJOR: Remove this check
502
                        } elseif (method_exists($fieldDescription->getAdmin(), 'trans')) {
503
                            $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...
504
                        }
505
                    }
506
507
                    $xEditableChoices[] = [
508
                        'value' => $value,
509
                        'text' => $text,
510
                    ];
511
                }
512
            }
513
        }
514
515
        if (false === $fieldDescription->getOption('required', true)
516
            && false === $fieldDescription->getOption('multiple', false)
517
        ) {
518
            $xEditableChoices = array_merge([[
519
                'value' => '',
520
                'text' => '',
521
            ]], $xEditableChoices);
522
        }
523
524
        return $xEditableChoices;
525
    }
526
527
    /**
528
     * Returns a canonicalized locale for "moment" NPM library,
529
     * or `null` if the locale's language is "en", which doesn't require localization.
530
     *
531
     * @return string|null
532
     */
533
    final public function getCanonicalizedLocaleForMoment(array $context)
534
    {
535
        $locale = strtolower(str_replace('_', '-', $context['app']->getRequest()->getLocale()));
536
537
        // "en" language doesn't require localization.
538
        if (('en' === $lang = substr($locale, 0, 2)) && !\in_array($locale, ['en-au', 'en-ca', 'en-gb', 'en-ie', 'en-nz'], true)) {
539
            return null;
540
        }
541
542
        foreach (self::MOMENT_UNSUPPORTED_LOCALES as $language => $locales) {
543
            if ($language === $lang && !\in_array($locale, $locales, true)) {
544
                $locale = $language;
545
            }
546
        }
547
548
        return $locale;
549
    }
550
551
    /**
552
     * Returns a canonicalized locale for "select2" NPM library,
553
     * or `null` if the locale's language is "en", which doesn't require localization.
554
     *
555
     * @return string|null
556
     */
557
    final public function getCanonicalizedLocaleForSelect2(array $context)
558
    {
559
        $locale = str_replace('_', '-', $context['app']->getRequest()->getLocale());
560
561
        // "en" language doesn't require localization.
562
        if ('en' === $lang = substr($locale, 0, 2)) {
563
            return null;
564
        }
565
566
        switch ($locale) {
567
            case 'pt':
568
                $locale = 'pt-PT';
569
                break;
570
            case 'ug':
571
                $locale = 'ug-CN';
572
                break;
573
            case 'zh':
574
                $locale = 'zh-CN';
575
                break;
576
            default:
577
                if (!\in_array($locale, ['pt-BR', 'pt-PT', 'ug-CN', 'zh-CN', 'zh-TW'], true)) {
578
                    $locale = $lang;
579
                }
580
        }
581
582
        return $locale;
583
    }
584
585
    /**
586
     * @param string|array $role
587
     * @param object|null  $object
588
     * @param string|null  $field
589
     *
590
     * @return bool
591
     */
592
    public function isGrantedAffirmative($role, $object = null, $field = null)
593
    {
594
        if (null === $this->securityChecker) {
595
            return false;
596
        }
597
598
        if (null !== $field) {
599
            $object = new FieldVote($object, $field);
600
        }
601
602
        if (!\is_array($role)) {
603
            $role = [$role];
604
        }
605
606
        foreach ($role as $oneRole) {
607
            try {
608
                if ($this->securityChecker->isGranted($oneRole, $object)) {
609
                    return true;
610
                }
611
            } catch (AuthenticationCredentialsNotFoundException $e) {
612
                // empty on purpose
613
            }
614
        }
615
616
        return false;
617
    }
618
619
    /**
620
     * Get template.
621
     *
622
     * @param string $defaultTemplate
623
     *
624
     * @return TemplateWrapper
625
     */
626
    protected function getTemplate(
627
        FieldDescriptionInterface $fieldDescription,
628
        $defaultTemplate,
629
        Environment $environment
630
    ) {
631
        $templateName = $fieldDescription->getTemplate() ?: $defaultTemplate;
632
633
        try {
634
            $template = $environment->load($templateName);
635
        } catch (LoaderError $e) {
636
            @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...
637
                'Relying on default template loading on field template loading exception is deprecated since 3.1'
638
                .' and will be removed in 4.0. A %s exception will be thrown instead',
639
                LoaderError::class
640
            ), E_USER_DEPRECATED);
641
            $template = $environment->load($defaultTemplate);
642
643
            if (null !== $this->logger) {
644
                $this->logger->warning(sprintf(
645
                    'An error occured trying to load the template "%s" for the field "%s",'
646
                    .' the default template "%s" was used instead.',
647
                    $templateName,
648
                    $fieldDescription->getFieldName(),
649
                    $defaultTemplate
650
                ), ['exception' => $e]);
651
            }
652
        }
653
654
        return $template;
655
    }
656
657
    private function render(
658
        FieldDescriptionInterface $fieldDescription,
659
        TemplateWrapper $template,
660
        array $parameters,
661
        Environment $environment
662
    ): ?string {
663
        $content = $template->render($parameters);
664
665
        if ($environment->isDebug()) {
666
            $commentTemplate = <<<'EOT'
667
668
<!-- START
669
    fieldName: %s
670
    template: %s
671
    compiled template: %s
672
    -->
673
    %s
674
<!-- END - fieldName: %s -->
675
EOT;
676
677
            return sprintf(
678
                $commentTemplate,
679
                $fieldDescription->getFieldName(),
680
                $fieldDescription->getTemplate(),
681
                $template->getSourceContext()->getName(),
682
                $content,
683
                $fieldDescription->getFieldName()
684
            );
685
        }
686
687
        return $content;
688
    }
689
690
    /**
691
     * @throws ServiceCircularReferenceException
692
     * @throws ServiceNotFoundException
693
     */
694
    private function getTemplateRegistry(string $adminCode): TemplateRegistryInterface
695
    {
696
        $serviceId = $adminCode.'.template_registry';
697
        $templateRegistry = $this->templateRegistries->get($serviceId);
698
699
        if ($templateRegistry instanceof TemplateRegistryInterface) {
700
            return $templateRegistry;
701
        }
702
703
        throw new ServiceNotFoundException($serviceId);
704
    }
705
}
706