Completed
Push — master ( 5c73c5...651a01 )
by Javier
17s queued 11s
created

SonataAdminExtension::renderViewElementCompare()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 61

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 61
rs 8.8509
c 0
b 0
f 0
cc 3
nc 4
nop 4

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Contracts\Translation\TranslatorInterface;
30
use Twig\Environment;
31
use Twig\Extension\AbstractExtension;
32
use Twig\TemplateWrapper;
33
use Twig\TwigFilter;
34
use Twig\TwigFunction;
35
36
/**
37
 * @final since sonata-project/admin-bundle 3.52
38
 *
39
 * @author Thomas Rabaix <[email protected]>
40
 */
41
final class SonataAdminExtension extends AbstractExtension
42
{
43
    // @todo: there are more locales which are not supported by moment and they need to be translated/normalized/canonicalized here
44
    public const MOMENT_UNSUPPORTED_LOCALES = [
45
        'de' => ['de', 'de-at'],
46
        'es' => ['es', 'es-do'],
47
        'nl' => ['nl', 'nl-be'],
48
        'fr' => ['fr', 'fr-ca', 'fr-ch'],
49
    ];
50
51
    /**
52
     * @var TranslatorInterface|null
53
     */
54
    protected $translator;
55
    /**
56
     * @var Pool
57
     */
58
    private $pool;
59
60
    /**
61
     * @var LoggerInterface
62
     */
63
    private $logger;
64
65
    /**
66
     * @var string[]
67
     */
68
    private $xEditableTypeMapping = [];
69
70
    /**
71
     * @var ContainerInterface
72
     */
73
    private $templateRegistries;
74
75
    /**
76
     * @var AuthorizationCheckerInterface
77
     */
78
    private $securityChecker;
79
80
    public function __construct(
81
        Pool $pool,
82
        ?LoggerInterface $logger = null,
83
        TranslatorInterface $translator,
84
        ?ContainerInterface $templateRegistries = null,
85
        ?AuthorizationCheckerInterface $securityChecker = null
86
    ) {
87
        $this->pool = $pool;
88
        $this->logger = $logger;
89
        $this->translator = $translator;
90
        $this->templateRegistries = $templateRegistries;
91
        $this->securityChecker = $securityChecker;
92
    }
93
94
    public function getFilters()
95
    {
96
        return [
97
            new TwigFilter(
98
                'render_list_element',
99
                [$this, 'renderListElement'],
100
                [
101
                    'is_safe' => ['html'],
102
                    'needs_environment' => true,
103
                ]
104
            ),
105
            new TwigFilter(
106
                'render_view_element',
107
                [$this, 'renderViewElement'],
108
                [
109
                    'is_safe' => ['html'],
110
                    'needs_environment' => true,
111
                ]
112
            ),
113
            new TwigFilter(
114
                'render_view_element_compare',
115
                [$this, 'renderViewElementCompare'],
116
                [
117
                    'is_safe' => ['html'],
118
                    'needs_environment' => true,
119
                ]
120
            ),
121
            new TwigFilter(
122
                'render_relation_element',
123
                [$this, 'renderRelationElement']
124
            ),
125
            new TwigFilter(
126
                'sonata_urlsafeid',
127
                [$this, 'getUrlSafeIdentifier']
128
            ),
129
            new TwigFilter(
130
                'sonata_xeditable_type',
131
                [$this, 'getXEditableType']
132
            ),
133
            new TwigFilter(
134
                'sonata_xeditable_choices',
135
                [$this, 'getXEditableChoices']
136
            ),
137
        ];
138
    }
139
140
    public function getFunctions()
141
    {
142
        return [
143
            new TwigFunction('canonicalize_locale_for_moment', [$this, 'getCanonicalizedLocaleForMoment'], ['needs_context' => true]),
144
            new TwigFunction('canonicalize_locale_for_select2', [$this, 'getCanonicalizedLocaleForSelect2'], ['needs_context' => true]),
145
            new TwigFunction('is_granted_affirmative', [$this, 'isGrantedAffirmative']),
146
        ];
147
    }
148
149
    public function getName()
150
    {
151
        return 'sonata_admin';
152
    }
153
154
    /**
155
     * render a list element from the FieldDescription.
156
     *
157
     * @param object $object
158
     * @param array  $params
159
     *
160
     * @return string
161
     */
162
    public function renderListElement(
163
        Environment $environment,
164
        $object,
165
        FieldDescriptionInterface $fieldDescription,
166
        $params = []
167
    ) {
168
        $template = $this->getTemplate(
169
            $fieldDescription,
170
            $this->getTemplateRegistry($fieldDescription->getAdmin()->getCode())->getTemplate('base_list_field'),
171
            $environment
172
        );
173
174
        return $this->render($fieldDescription, $template, array_merge($params, [
175
            'admin' => $fieldDescription->getAdmin(),
176
            'object' => $object,
177
            'value' => $this->getValueFromFieldDescription($object, $fieldDescription),
178
            'field_description' => $fieldDescription,
179
        ]), $environment);
180
    }
181
182
    /**
183
     * render a view element.
184
     *
185
     * @param object $object
186
     *
187
     * @return string
188
     */
189
    public function renderViewElement(
190
        Environment $environment,
191
        FieldDescriptionInterface $fieldDescription,
192
        $object
193
    ) {
194
        $template = $this->getTemplate(
195
            $fieldDescription,
196
            '@SonataAdmin/CRUD/base_show_field.html.twig',
197
            $environment
198
        );
199
200
        try {
201
            $value = $fieldDescription->getValue($object);
202
        } catch (NoValueException $e) {
203
            // NEXT_MAJOR: Remove the try catch in order to throw the NoValueException.
204
            @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...
205
                'Accessing a non existing value is deprecated'
206
                .' since sonata-project/admin-bundle 3.x and will throw an exception in 4.0.',
207
                E_USER_DEPRECATED
208
            );
209
210
            $value = null;
211
        }
212
213
        return $this->render($fieldDescription, $template, [
214
            'field_description' => $fieldDescription,
215
            'object' => $object,
216
            'value' => $value,
217
            'admin' => $fieldDescription->getAdmin(),
218
        ], $environment);
219
    }
220
221
    /**
222
     * render a compared view element.
223
     *
224
     * @param mixed $baseObject
225
     * @param mixed $compareObject
226
     *
227
     * @return string
228
     */
229
    public function renderViewElementCompare(
230
        Environment $environment,
231
        FieldDescriptionInterface $fieldDescription,
232
        $baseObject,
233
        $compareObject
234
    ) {
235
        $template = $this->getTemplate(
236
            $fieldDescription,
237
            '@SonataAdmin/CRUD/base_show_field.html.twig',
238
            $environment
239
        );
240
241
        try {
242
            $baseValue = $fieldDescription->getValue($baseObject);
243
        } catch (NoValueException $e) {
244
            // NEXT_MAJOR: Remove the try catch in order to throw the NoValueException.
245
            @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...
246
                'Accessing a non existing value is deprecated'
247
                .' since sonata-project/admin-bundle 3.x and will throw an exception in 4.0.',
248
                E_USER_DEPRECATED
249
            );
250
251
            $baseValue = null;
252
        }
253
254
        try {
255
            $compareValue = $fieldDescription->getValue($compareObject);
256
        } catch (NoValueException $e) {
257
            // NEXT_MAJOR: Remove the try catch in order to throw the NoValueException.
258
            @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...
259
                'Accessing a non existing value is deprecated'
260
                .' since sonata-project/admin-bundle 3.x and will throw an exception in 4.0.',
261
                E_USER_DEPRECATED
262
            );
263
264
            $compareValue = null;
265
        }
266
267
        $baseValueOutput = $template->render([
268
            'admin' => $fieldDescription->getAdmin(),
269
            'field_description' => $fieldDescription,
270
            'value' => $baseValue,
271
        ]);
272
273
        $compareValueOutput = $template->render([
274
            'field_description' => $fieldDescription,
275
            'admin' => $fieldDescription->getAdmin(),
276
            'value' => $compareValue,
277
        ]);
278
279
        // Compare the rendered output of both objects by using the (possibly) overridden field block
280
        $isDiff = $baseValueOutput !== $compareValueOutput;
281
282
        return $this->render($fieldDescription, $template, [
283
            'field_description' => $fieldDescription,
284
            'value' => $baseValue,
285
            'value_compare' => $compareValue,
286
            'is_diff' => $isDiff,
287
            'admin' => $fieldDescription->getAdmin(),
288
        ], $environment);
289
    }
290
291
    /**
292
     * @param mixed $element
293
     *
294
     * @throws \RuntimeException
295
     *
296
     * @return mixed
297
     */
298
    public function renderRelationElement($element, FieldDescriptionInterface $fieldDescription)
299
    {
300
        if (!\is_object($element)) {
301
            return $element;
302
        }
303
304
        $propertyPath = $fieldDescription->getOption('associated_property');
305
306
        if (null === $propertyPath) {
307
            // For BC kept associated_tostring option behavior
308
            $method = $fieldDescription->getOption('associated_tostring');
309
310
            if ($method) {
311
                @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...
312
                    'Option "associated_tostring" is deprecated since version 2.3 and will be removed in 4.0. '
313
                    .'Use "associated_property" instead.',
314
                    E_USER_DEPRECATED
315
                );
316
            } else {
317
                $method = '__toString';
318
            }
319
320
            if (!method_exists($element, $method)) {
321
                throw new \RuntimeException(sprintf(
322
                    'You must define an `associated_property` option or '.
323
                    'create a `%s::__toString` method to the field option %s from service %s is ',
324
                    \get_class($element),
325
                    $fieldDescription->getName(),
326
                    $fieldDescription->getAdmin()->getCode()
327
                ));
328
            }
329
330
            return $element->$method();
331
        }
332
333
        if (\is_callable($propertyPath)) {
334
            return $propertyPath($element);
335
        }
336
337
        return $this->pool->getPropertyAccessor()->getValue($element, $propertyPath);
338
    }
339
340
    /**
341
     * Get the identifiers as a string that is safe to use in a url.
342
     *
343
     * @param object $model
344
     *
345
     * @return string string representation of the id that is safe to use in a url
346
     */
347
    public function getUrlSafeIdentifier($model, ?AdminInterface $admin = null)
348
    {
349
        if (null === $admin) {
350
            $admin = $this->pool->getAdminByClass(ClassUtils::getClass($model));
351
        }
352
353
        return $admin->getUrlSafeIdentifier($model);
354
    }
355
356
    /**
357
     * @param string[] $xEditableTypeMapping
358
     */
359
    public function setXEditableTypeMapping($xEditableTypeMapping): void
360
    {
361
        $this->xEditableTypeMapping = $xEditableTypeMapping;
362
    }
363
364
    /**
365
     * @return string|bool
366
     */
367
    public function getXEditableType($type)
368
    {
369
        return isset($this->xEditableTypeMapping[$type]) ? $this->xEditableTypeMapping[$type] : false;
370
    }
371
372
    /**
373
     * Return xEditable choices based on the field description choices options & catalogue options.
374
     * With the following choice options:
375
     *     ['Status1' => 'Alias1', 'Status2' => 'Alias2']
376
     * The method will return:
377
     *     [['value' => 'Status1', 'text' => 'Alias1'], ['value' => 'Status2', 'text' => 'Alias2']].
378
     *
379
     * @return array
380
     */
381
    public function getXEditableChoices(FieldDescriptionInterface $fieldDescription)
382
    {
383
        $choices = $fieldDescription->getOption('choices', []);
384
        $catalogue = $fieldDescription->getOption('catalogue');
385
        $xEditableChoices = [];
386
        if (!empty($choices)) {
387
            reset($choices);
388
            $first = current($choices);
389
            // the choices are already in the right format
390
            if (\is_array($first) && \array_key_exists('value', $first) && \array_key_exists('text', $first)) {
391
                $xEditableChoices = $choices;
392
            } else {
393
                foreach ($choices as $value => $text) {
394
                    if ($catalogue) {
395
                        if (null !== $this->translator) {
396
                            $text = $this->translator->trans($text, [], $catalogue);
397
                        // NEXT_MAJOR: Remove this check
398
                        } elseif (method_exists($fieldDescription->getAdmin(), 'trans')) {
399
                            $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...
400
                        }
401
                    }
402
403
                    $xEditableChoices[] = [
404
                        'value' => $value,
405
                        'text' => $text,
406
                    ];
407
                }
408
            }
409
        }
410
411
        if (false === $fieldDescription->getOption('required', true)
412
            && false === $fieldDescription->getOption('multiple', false)
413
        ) {
414
            $xEditableChoices = array_merge([[
415
                'value' => '',
416
                'text' => '',
417
            ]], $xEditableChoices);
418
        }
419
420
        return $xEditableChoices;
421
    }
422
423
    /*
424
     * Returns a canonicalized locale for "moment" NPM library,
425
     * or `null` if the locale's language is "en", which doesn't require localization.
426
     *
427
     * @return string|null
428
     */
429
    public function getCanonicalizedLocaleForMoment(array $context)
430
    {
431
        $locale = strtolower(str_replace('_', '-', $context['app']->getRequest()->getLocale()));
432
433
        // "en" language doesn't require localization.
434
        if (('en' === $lang = substr($locale, 0, 2)) && !\in_array($locale, ['en-au', 'en-ca', 'en-gb', 'en-ie', 'en-nz'], true)) {
435
            return null;
436
        }
437
438
        foreach (self::MOMENT_UNSUPPORTED_LOCALES as $language => $locales) {
439
            if ($language === $lang && !\in_array($locale, $locales, true)) {
440
                $locale = $language;
441
            }
442
        }
443
444
        return $locale;
445
    }
446
447
    /**
448
     * Returns a canonicalized locale for "select2" NPM library,
449
     * or `null` if the locale's language is "en", which doesn't require localization.
450
     *
451
     * @return string|null
452
     */
453
    public function getCanonicalizedLocaleForSelect2(array $context)
454
    {
455
        $locale = str_replace('_', '-', $context['app']->getRequest()->getLocale());
456
457
        // "en" language doesn't require localization.
458
        if ('en' === $lang = substr($locale, 0, 2)) {
459
            return null;
460
        }
461
462
        switch ($locale) {
463
            case 'pt':
464
                $locale = 'pt-PT';
465
                break;
466
            case 'ug':
467
                $locale = 'ug-CN';
468
                break;
469
            case 'zh':
470
                $locale = 'zh-CN';
471
                break;
472
            default:
473
                if (!\in_array($locale, ['pt-BR', 'pt-PT', 'ug-CN', 'zh-CN', 'zh-TW'], true)) {
474
                    $locale = $lang;
475
                }
476
        }
477
478
        return $locale;
479
    }
480
481
    /**
482
     * @param string|array $role
483
     * @param object|null  $object
484
     * @param string|null  $field
485
     *
486
     * @return bool
487
     */
488
    public function isGrantedAffirmative($role, $object = null, $field = null)
489
    {
490
        if (null === $this->securityChecker) {
491
            return false;
492
        }
493
494
        if (null !== $field) {
495
            $object = new FieldVote($object, $field);
496
        }
497
498
        if (!\is_array($role)) {
499
            $role = [$role];
500
        }
501
502
        foreach ($role as $oneRole) {
503
            try {
504
                if ($this->securityChecker->isGranted($oneRole, $object)) {
505
                    return true;
506
                }
507
            } catch (AuthenticationCredentialsNotFoundException $e) {
508
                // empty on purpose
509
            }
510
        }
511
512
        return false;
513
    }
514
515
    /**
516
     * return the value related to FieldDescription, if the associated object does no
517
     * exists => a temporary one is created.
518
     *
519
     * @param object $object
520
     *
521
     * @throws \RuntimeException
522
     *
523
     * @return mixed
524
     */
525
    private function getValueFromFieldDescription(
526
        $object,
527
        FieldDescriptionInterface $fieldDescription,
528
        array $params = []
529
    ) {
530
        if (isset($params['loop']) && $object instanceof \ArrayAccess) {
531
            throw new \RuntimeException('remove the loop requirement');
532
        }
533
534
        $value = null;
535
536
        try {
537
            $value = $fieldDescription->getValue($object);
538
        } catch (NoValueException $e) {
539
            if ($fieldDescription->getAssociationAdmin()) {
540
                $value = $fieldDescription->getAssociationAdmin()->getNewInstance();
541
            }
542
        }
543
544
        return $value;
545
    }
546
547
    /**
548
     * Get template.
549
     *
550
     * @param string $defaultTemplate
551
     *
552
     * @return TemplateWrapper
553
     */
554
    private function getTemplate(
555
        FieldDescriptionInterface $fieldDescription,
556
        $defaultTemplate,
557
        Environment $environment
558
    ) {
559
        $templateName = $fieldDescription->getTemplate() ?: $defaultTemplate;
560
561
        try {
562
            $template = $environment->load($templateName);
563
        } catch (LoaderError $e) {
0 ignored issues
show
Bug introduced by
The class Sonata\AdminBundle\Twig\Extension\LoaderError does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
564
            @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...
565
                sprintf(
566
                    'Relying on default template loading on field template loading exception '.
567
                    'is deprecated since 3.1 and will be removed in 4.0. '.
568
                    'A %s exception will be thrown instead',
569
                    LoaderError::class
570
                ),
571
                E_USER_DEPRECATED
572
            );
573
            $template = $environment->load($defaultTemplate);
574
575
            if (null !== $this->logger) {
576
                $this->logger->warning(sprintf(
577
                    'An error occured trying to load the template "%s" for the field "%s", '.
578
                    'the default template "%s" was used instead.',
579
                    $templateName,
580
                    $fieldDescription->getFieldName(),
581
                    $defaultTemplate
582
                ), ['exception' => $e]);
583
            }
584
        }
585
586
        return $template;
587
    }
588
589
    private function render(
590
        FieldDescriptionInterface $fieldDescription,
591
        TemplateWrapper $template,
592
        array $parameters,
593
        Environment $environment
594
    ): ?string {
595
        $content = $template->render($parameters);
596
597
        if ($environment->isDebug()) {
598
            $commentTemplate = <<<'EOT'
599
600
<!-- START
601
    fieldName: %s
602
    template: %s
603
    compiled template: %s
604
    -->
605
    %s
606
<!-- END - fieldName: %s -->
607
EOT;
608
609
            return sprintf(
610
                $commentTemplate,
611
                $fieldDescription->getFieldName(),
612
                $fieldDescription->getTemplate(),
613
                $template->getSourceContext()->getName(),
614
                $content,
615
                $fieldDescription->getFieldName()
616
            );
617
        }
618
619
        return $content;
620
    }
621
622
    /**
623
     * @throws ServiceCircularReferenceException
624
     * @throws ServiceNotFoundException
625
     */
626
    private function getTemplateRegistry(string $adminCode): TemplateRegistryInterface
627
    {
628
        $serviceId = $adminCode.'.template_registry';
629
        $templateRegistry = $this->templateRegistries->get($serviceId);
630
631
        if ($templateRegistry instanceof TemplateRegistryInterface) {
632
            return $templateRegistry;
633
        }
634
635
        throw new ServiceNotFoundException($serviceId);
636
    }
637
}
638