Completed
Push — 3.x ( 871dd6...595a7a )
by Marko
05:19
created

SonataAdminExtension   F

Complexity

Total Complexity 67

Size/Duplication

Total Lines 605
Duplicated Lines 0 %

Coupling/Cohesion

Components 5
Dependencies 17

Importance

Changes 0
Metric Value
wmc 67
lcom 5
cbo 17
dl 0
loc 605
rs 3.035
c 0
b 0
f 0

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 20 2
A getFilters() 0 45 1
A getFunctions() 0 8 1
A getName() 0 4 1
A renderListElement() 0 21 1
A output() 0 13 1
A getValueFromFieldDescription() 0 21 5
A renderViewElement() 0 24 2
A renderViewElementCompare() 0 47 3
B renderRelationElement() 0 41 6
A getUrlsafeIdentifier() 0 8 2
A setXEditableTypeMapping() 0 4 1
A getXEditableType() 0 4 2
B getXEditableChoices() 0 41 11
B getCanonicalizedLocaleForMoment() 0 21 7
B getCanonicalizedLocaleForSelect2() 0 27 6
B isGrantedAffirmative() 0 26 7
A getTemplate() 0 31 4
A render() 0 32 2
A getTemplateRegistry() 0 11 2

How to fix   Complexity   

Complex Class

Complex classes like SonataAdminExtension often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SonataAdminExtension, and based on these observations, apply Extract Interface, too.

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