Completed
Push — 3.x ( 799626...732596 )
by Grégoire
04:11
created

SonataAdminExtension::getTemplateRegistry()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 6
nc 2
nop 1
1
<?php
2
3
/*
4
 * This file is part of the Sonata Project package.
5
 *
6
 * (c) Thomas Rabaix <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Sonata\AdminBundle\Twig\Extension;
13
14
use Doctrine\Common\Util\ClassUtils;
15
use Psr\Log\LoggerInterface;
16
use Sonata\AdminBundle\Admin\AdminInterface;
17
use Sonata\AdminBundle\Admin\FieldDescriptionInterface;
18
use Sonata\AdminBundle\Admin\Pool;
19
use Sonata\AdminBundle\Exception\NoValueException;
20
use Sonata\AdminBundle\Templating\TemplateRegistryInterface;
21
use Symfony\Component\DependencyInjection\ContainerInterface;
22
use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException;
23
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
24
use Symfony\Component\Translation\TranslatorInterface;
25
use Twig\Environment;
26
use Twig\Error\LoaderError;
27
use Twig\Extension\AbstractExtension;
28
use Twig\Template;
29
use Twig\TemplateWrapper;
30
use Twig\TwigFilter;
31
use Twig\TwigFunction;
32
33
/**
34
 * @author Thomas Rabaix <[email protected]>
35
 */
36
class SonataAdminExtension extends AbstractExtension
37
{
38
    /**
39
     * @var Pool
40
     */
41
    protected $pool;
42
43
    /**
44
     * @var LoggerInterface
45
     */
46
    protected $logger;
47
48
    /**
49
     * @var TranslatorInterface|null
50
     */
51
    protected $translator;
52
53
    /**
54
     * @var string[]
55
     */
56
    private $xEditableTypeMapping = [];
57
58
    /**
59
     * @var ContainerInterface
60
     */
61
    private $templateRegistries;
62
63
    public function __construct(
64
        Pool $pool,
65
        LoggerInterface $logger = null,
66
        TranslatorInterface $translator = null,
67
        ContainerInterface $templateRegistries = null
68
    ) {
69
        // NEXT_MAJOR: make the translator parameter required
70
        if (null === $translator) {
71
            @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...
72
                'The $translator parameter will be required fields with the 4.0 release.',
73
                E_USER_DEPRECATED
74
            );
75
        }
76
        $this->pool = $pool;
77
        $this->logger = $logger;
78
        $this->translator = $translator;
79
        $this->templateRegistries = $templateRegistries;
80
    }
81
82
    public function getFilters()
83
    {
84
        return [
85
            new TwigFilter(
86
                'render_list_element',
87
                [$this, 'renderListElement'],
88
                [
89
                    'is_safe' => ['html'],
90
                    'needs_environment' => true,
91
                ]
92
            ),
93
            new TwigFilter(
94
                'render_view_element',
95
                [$this, 'renderViewElement'],
96
                [
97
                    'is_safe' => ['html'],
98
                    'needs_environment' => true,
99
                ]
100
            ),
101
            new TwigFilter(
102
                'render_view_element_compare',
103
                [$this, 'renderViewElementCompare'],
104
                [
105
                    'is_safe' => ['html'],
106
                    'needs_environment' => true,
107
                ]
108
            ),
109
            new TwigFilter(
110
                'render_relation_element',
111
                [$this, 'renderRelationElement']
112
            ),
113
            new TwigFilter(
114
                'sonata_urlsafeid',
115
                [$this, 'getUrlsafeIdentifier']
116
            ),
117
            new TwigFilter(
118
                'sonata_xeditable_type',
119
                [$this, 'getXEditableType']
120
            ),
121
            new TwigFilter(
122
                'sonata_xeditable_choices',
123
                [$this, 'getXEditableChoices']
124
            ),
125
        ];
126
    }
127
128
    public function getFunctions()
129
    {
130
        return [
131
            new TwigFunction('canonicalize_locale_for_moment', [$this, 'getCanonicalizedLocaleForMoment'], ['needs_context' => true]),
132
            new TwigFunction('canonicalize_locale_for_select2', [$this, 'getCanonicalizedLocaleForSelect2'], ['needs_context' => true]),
133
        ];
134
    }
135
136
    public function getName()
137
    {
138
        return 'sonata_admin';
139
    }
140
141
    /**
142
     * render a list element from the FieldDescription.
143
     *
144
     * @param mixed $object
145
     * @param array $params
146
     *
147
     * @return string
148
     */
149
    public function renderListElement(
150
        Environment $environment,
151
        $object,
152
        FieldDescriptionInterface $fieldDescription,
153
        $params = []
154
    ) {
155
        $template = $this->getTemplate(
156
            $fieldDescription,
157
            $this->getTemplateRegistry($fieldDescription->getAdmin()->getCode())->getTemplate('base_list_field'),
158
            $environment
159
        );
160
161
        return $this->render($fieldDescription, $template, array_merge($params, [
162
            'admin' => $fieldDescription->getAdmin(),
163
            'object' => $object,
164
            'value' => $this->getValueFromFieldDescription($object, $fieldDescription),
165
            'field_description' => $fieldDescription,
166
        ]), $environment);
167
    }
168
169
    /**
170
     * @deprecated since 3.33, to be removed in 4.0. Use render instead
171
     *
172
     * @return string
173
     */
174
    public function output(
175
        FieldDescriptionInterface $fieldDescription,
176
        Template $template,
177
        array $parameters,
178
        Environment $environment
179
    ) {
180
        return $this->render(
181
            $fieldDescription,
182
            new TemplateWrapper($environment, $template),
183
            $parameters,
184
            $environment
185
        );
186
    }
187
188
    /**
189
     * return the value related to FieldDescription, if the associated object does no
190
     * exists => a temporary one is created.
191
     *
192
     * @param object $object
193
     *
194
     * @throws \RuntimeException
195
     *
196
     * @return mixed
197
     */
198
    public function getValueFromFieldDescription(
199
        $object,
200
        FieldDescriptionInterface $fieldDescription,
201
        array $params = []
202
    ) {
203
        if (isset($params['loop']) && $object instanceof \ArrayAccess) {
204
            throw new \RuntimeException('remove the loop requirement');
205
        }
206
207
        $value = null;
208
209
        try {
210
            $value = $fieldDescription->getValue($object);
211
        } catch (NoValueException $e) {
212
            if ($fieldDescription->getAssociationAdmin()) {
213
                $value = $fieldDescription->getAssociationAdmin()->getNewInstance();
214
            }
215
        }
216
217
        return $value;
218
    }
219
220
    /**
221
     * render a view element.
222
     *
223
     * @param mixed $object
224
     *
225
     * @return string
226
     */
227
    public function renderViewElement(
228
        Environment $environment,
229
        FieldDescriptionInterface $fieldDescription,
230
        $object
231
    ) {
232
        $template = $this->getTemplate(
233
            $fieldDescription,
234
            '@SonataAdmin/CRUD/base_show_field.html.twig',
235
            $environment
236
        );
237
238
        try {
239
            $value = $fieldDescription->getValue($object);
240
        } catch (NoValueException $e) {
241
            $value = null;
242
        }
243
244
        return $this->render($fieldDescription, $template, [
245
            'field_description' => $fieldDescription,
246
            'object' => $object,
247
            'value' => $value,
248
            'admin' => $fieldDescription->getAdmin(),
249
        ], $environment);
250
    }
251
252
    /**
253
     * render a compared view element.
254
     *
255
     * @param mixed $baseObject
256
     * @param mixed $compareObject
257
     *
258
     * @return string
259
     */
260
    public function renderViewElementCompare(
261
        Environment $environment,
262
        FieldDescriptionInterface $fieldDescription,
263
        $baseObject,
264
        $compareObject
265
    ) {
266
        $template = $this->getTemplate(
267
            $fieldDescription,
268
            '@SonataAdmin/CRUD/base_show_field.html.twig',
269
            $environment
270
        );
271
272
        try {
273
            $baseValue = $fieldDescription->getValue($baseObject);
274
        } catch (NoValueException $e) {
275
            $baseValue = null;
276
        }
277
278
        try {
279
            $compareValue = $fieldDescription->getValue($compareObject);
280
        } catch (NoValueException $e) {
281
            $compareValue = null;
282
        }
283
284
        $baseValueOutput = $template->render([
285
            'admin' => $fieldDescription->getAdmin(),
286
            'field_description' => $fieldDescription,
287
            'value' => $baseValue,
288
        ]);
289
290
        $compareValueOutput = $template->render([
291
            'field_description' => $fieldDescription,
292
            'admin' => $fieldDescription->getAdmin(),
293
            'value' => $compareValue,
294
        ]);
295
296
        // Compare the rendered output of both objects by using the (possibly) overridden field block
297
        $isDiff = $baseValueOutput !== $compareValueOutput;
298
299
        return $this->render($fieldDescription, $template, [
300
            'field_description' => $fieldDescription,
301
            'value' => $baseValue,
302
            'value_compare' => $compareValue,
303
            'is_diff' => $isDiff,
304
            'admin' => $fieldDescription->getAdmin(),
305
        ], $environment);
306
    }
307
308
    /**
309
     * @param mixed $element
310
     *
311
     * @throws \RuntimeException
312
     *
313
     * @return mixed
314
     */
315
    public function renderRelationElement($element, FieldDescriptionInterface $fieldDescription)
316
    {
317
        if (!is_object($element)) {
318
            return $element;
319
        }
320
321
        $propertyPath = $fieldDescription->getOption('associated_property');
322
323
        if (null === $propertyPath) {
324
            // For BC kept associated_tostring option behavior
325
            $method = $fieldDescription->getOption('associated_tostring');
326
327
            if ($method) {
328
                @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...
329
                    'Option "associated_tostring" is deprecated since version 2.3 and will be removed in 4.0. '
330
                    .'Use "associated_property" instead.',
331
                    E_USER_DEPRECATED
332
                );
333
            } else {
334
                $method = '__toString';
335
            }
336
337
            if (!method_exists($element, $method)) {
338
                throw new \RuntimeException(sprintf(
339
                    'You must define an `associated_property` option or '.
340
                    'create a `%s::__toString` method to the field option %s from service %s is ',
341
                    get_class($element),
342
                    $fieldDescription->getName(),
343
                    $fieldDescription->getAdmin()->getCode()
344
                ));
345
            }
346
347
            return call_user_func([$element, $method]);
348
        }
349
350
        if (is_callable($propertyPath)) {
351
            return $propertyPath($element);
352
        }
353
354
        return $this->pool->getPropertyAccessor()->getValue($element, $propertyPath);
0 ignored issues
show
Documentation introduced by
$propertyPath is of type array, but the function expects a string|object<Symfony\Co...\PropertyPathInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
355
    }
356
357
    /**
358
     * Get the identifiers as a string that is safe to use in a url.
359
     *
360
     * @param object $model
361
     *
362
     * @return string string representation of the id that is safe to use in a url
363
     */
364
    public function getUrlsafeIdentifier($model, AdminInterface $admin = null)
365
    {
366
        if (null === $admin) {
367
            $admin = $this->pool->getAdminByClass(ClassUtils::getClass($model));
368
        }
369
370
        return $admin->getUrlsafeIdentifier($model);
0 ignored issues
show
Bug introduced by
It seems like $admin is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
371
    }
372
373
    /**
374
     * @param string[] $xEditableTypeMapping
375
     */
376
    public function setXEditableTypeMapping($xEditableTypeMapping)
377
    {
378
        $this->xEditableTypeMapping = $xEditableTypeMapping;
379
    }
380
381
    /**
382
     * @return string|bool
383
     */
384
    public function getXEditableType($type)
385
    {
386
        return isset($this->xEditableTypeMapping[$type]) ? $this->xEditableTypeMapping[$type] : false;
387
    }
388
389
    /**
390
     * Return xEditable choices based on the field description choices options & catalogue options.
391
     * With the following choice options:
392
     *     ['Status1' => 'Alias1', 'Status2' => 'Alias2']
393
     * The method will return:
394
     *     [['value' => 'Status1', 'text' => 'Alias1'], ['value' => 'Status2', 'text' => 'Alias2']].
395
     *
396
     * @return array
397
     */
398
    public function getXEditableChoices(FieldDescriptionInterface $fieldDescription)
399
    {
400
        $choices = $fieldDescription->getOption('choices', []);
401
        $catalogue = $fieldDescription->getOption('catalogue');
402
        $xEditableChoices = [];
403
        if (!empty($choices)) {
404
            reset($choices);
405
            $first = current($choices);
406
            // the choices are already in the right format
407
            if (is_array($first) && array_key_exists('value', $first) && array_key_exists('text', $first)) {
408
                $xEditableChoices = $choices;
409
            } else {
410
                foreach ($choices as $value => $text) {
411
                    if ($catalogue) {
412
                        if (null !== $this->translator) {
413
                            $text = $this->translator->trans($text, [], $catalogue);
0 ignored issues
show
Documentation introduced by
$catalogue is of type array, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
414
                        // NEXT_MAJOR: Remove this check
415
                        } elseif (method_exists($fieldDescription->getAdmin(), 'trans')) {
416
                            $text = $fieldDescription->getAdmin()->trans($text, [], $catalogue);
0 ignored issues
show
Documentation introduced by
$catalogue is of type array, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Deprecated Code introduced by
The method Sonata\AdminBundle\Admin\AdminInterface::trans() has been deprecated with message: since 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...
417
                        }
418
                    }
419
420
                    $xEditableChoices[] = [
421
                        'value' => $value,
422
                        'text' => $text,
423
                    ];
424
                }
425
            }
426
        }
427
428
        if (false === $fieldDescription->getOption('required', true)
429
            && false === $fieldDescription->getOption('multiple', false)
430
        ) {
431
            $xEditableChoices = array_merge([[
432
                'value' => '',
433
                'text' => '',
434
            ]], $xEditableChoices);
435
        }
436
437
        return $xEditableChoices;
438
    }
439
440
    /**
441
     * Returns a canonicalized locale for "moment" NPM library,
442
     * or `null` if the locale's language is "en", which doesn't require localization.
443
     *
444
     * @return null|string
445
     */
446
    final public function getCanonicalizedLocaleForMoment(array $context)
447
    {
448
        $locale = strtolower(str_replace('_', '-', $context['app']->getRequest()->getLocale()));
449
450
        // "en" language doesn't require localization.
451
        if (('en' === $lang = substr($locale, 0, 2)) && !in_array($locale, ['en-au', 'en-ca', 'en-gb', 'en-ie', 'en-nz'], true)) {
452
            return null;
453
        }
454
455
        if ('es' === $lang && !in_array($locale, ['es', 'es-do'], true)) {
456
            // `moment: ^2.8` only ships "es" and "es-do" locales for "es" language
457
            $locale = 'es';
458
        } elseif ('nl' === $lang && !in_array($locale, ['nl', 'nl-be'], true)) {
459
            // `moment: ^2.8` only ships "nl" and "nl-be" locales for "nl" language
460
            $locale = 'nl';
461
        }
462
463
        // @todo: there are more locales which are not supported by moment and they need to be translated/normalized/canonicalized here
464
465
        return $locale;
466
    }
467
468
    /**
469
     * Returns a canonicalized locale for "select2" NPM library,
470
     * or `null` if the locale's language is "en", which doesn't require localization.
471
     *
472
     * @return null|string
473
     */
474
    final public function getCanonicalizedLocaleForSelect2(array $context)
475
    {
476
        $locale = str_replace('_', '-', $context['app']->getRequest()->getLocale());
477
478
        // "en" language doesn't require localization.
479
        if ('en' === $lang = substr($locale, 0, 2)) {
480
            return null;
481
        }
482
483
        switch ($locale) {
484
            case 'pt':
485
                $locale = 'pt-PT';
486
                break;
487
            case 'ug':
488
                $locale = 'ug-CN';
489
                break;
490
            case 'zh':
491
                $locale = 'zh-CN';
492
                break;
493
            default:
494
                if (!in_array($locale, ['pt-BR', 'pt-PT', 'ug-CN', 'zh-CN', 'zh-TW'], true)) {
495
                    $locale = $lang;
496
                }
497
        }
498
499
        return $locale;
500
    }
501
502
    /**
503
     * Get template.
504
     *
505
     * @param string $defaultTemplate
506
     *
507
     * @return TemplateWrapper
508
     */
509
    protected function getTemplate(
510
        FieldDescriptionInterface $fieldDescription,
511
        $defaultTemplate,
512
        Environment $environment
513
    ) {
514
        $templateName = $fieldDescription->getTemplate() ?: $defaultTemplate;
515
516
        try {
517
            $template = $environment->load($templateName);
518
        } catch (LoaderError $e) {
519
            @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...
520
                'Relying on default template loading on field template loading exception '.
521
                'is deprecated since 3.1 and will be removed in 4.0. '.
522
                'A \Twig_Error_Loader exception will be thrown instead',
523
                E_USER_DEPRECATED
524
            );
525
            $template = $environment->load($defaultTemplate);
526
527
            if (null !== $this->logger) {
528
                $this->logger->warning(sprintf(
529
                    'An error occured trying to load the template "%s" for the field "%s", '.
530
                    'the default template "%s" was used instead.',
531
                    $templateName,
532
                    $fieldDescription->getFieldName(),
533
                    $defaultTemplate
534
                ), ['exception' => $e]);
535
            }
536
        }
537
538
        return $template;
539
    }
540
541
    /**
542
     * @return string
543
     */
544
    private function render(
545
        FieldDescriptionInterface $fieldDescription,
546
        TemplateWrapper $template,
547
        array $parameters,
548
        Environment $environment
549
    ) {
550
        $content = $template->render($parameters);
551
552
        if ($environment->isDebug()) {
553
            $commentTemplate = <<<'EOT'
554
555
<!-- START
556
    fieldName: %s
557
    template: %s
558
    compiled template: %s
559
    -->
560
    %s
561
<!-- END - fieldName: %s -->
562
EOT;
563
564
            return sprintf(
565
                $commentTemplate,
566
                $fieldDescription->getFieldName(),
567
                $fieldDescription->getTemplate(),
568
                $template->getSourceContext()->getName(),
569
                $content,
570
                $fieldDescription->getFieldName()
571
            );
572
        }
573
574
        return $content;
575
    }
576
577
    /**
578
     * @param string $adminCode
579
     *
580
     * @throws ServiceCircularReferenceException
581
     * @throws ServiceNotFoundException
582
     *
583
     * @return TemplateRegistryInterface
584
     */
585
    private function getTemplateRegistry($adminCode)
586
    {
587
        $serviceId = $adminCode.'.template_registry';
588
        $templateRegistry = $this->templateRegistries->get($serviceId);
589
590
        if ($templateRegistry instanceof TemplateRegistryInterface) {
591
            return $templateRegistry;
592
        }
593
594
        throw new ServiceNotFoundException($serviceId);
595
    }
596
}
597