Completed
Pull Request — master (#6210)
by Jordi Sala
03:57
created

SonataAdminExtension   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 522
Duplicated Lines 0 %

Coupling/Cohesion

Components 5
Dependencies 17

Importance

Changes 0
Metric Value
wmc 57
lcom 5
cbo 17
dl 0
loc 522
rs 5.04
c 0
b 0
f 0

19 Methods

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