Completed
Pull Request — master (#5032)
by Grégoire
03:41
created

SonataAdminExtension   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 541
Duplicated Lines 0 %

Coupling/Cohesion

Components 4
Dependencies 16

Importance

Changes 0
Metric Value
wmc 59
lcom 4
cbo 16
dl 0
loc 541
rs 4.5205
c 0
b 0
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 18 2
B getFilters() 0 45 1
A getFunctions() 0 7 1
A getName() 0 4 1
A renderListElement() 0 19 1
B 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
C getXEditableChoices() 0 41 11
B getCanonicalizedLocaleForMoment() 0 21 7
B getCanonicalizedLocaleForSelect2() 0 27 6
B getValueFromFieldDescription() 0 21 5
B getTemplate() 0 31 4
B 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 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\Translation\TranslatorInterface;
27
use Twig\Environment;
28
use Twig\Extension\AbstractExtension;
29
use Twig\Template;
30
use Twig\TemplateWrapper;
31
use Twig\TwigFilter;
32
use Twig\TwigFunction;
33
34
/**
35
 * @author Thomas Rabaix <[email protected]>
36
 */
37
final class SonataAdminExtension extends AbstractExtension
38
{
39
    /**
40
     * @var TranslatorInterface|null
41
     */
42
    protected $translator;
43
    /**
44
     * @var Pool
45
     */
46
    private $pool;
47
48
    /**
49
     * @var LoggerInterface
50
     */
51
    private $logger;
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
     * render a view element.
171
     *
172
     * @param mixed $object
173
     *
174
     * @return string
175
     */
176
    public function renderViewElement(
177
        Environment $environment,
178
        FieldDescriptionInterface $fieldDescription,
179
        $object
180
    ) {
181
        $template = $this->getTemplate(
182
            $fieldDescription,
183
            '@SonataAdmin/CRUD/base_show_field.html.twig',
184
            $environment
185
        );
186
187
        try {
188
            $value = $fieldDescription->getValue($object);
189
        } catch (NoValueException $e) {
190
            $value = null;
191
        }
192
193
        return $this->render($fieldDescription, $template, [
194
            'field_description' => $fieldDescription,
195
            'object' => $object,
196
            'value' => $value,
197
            'admin' => $fieldDescription->getAdmin(),
198
        ], $environment);
199
    }
200
201
    /**
202
     * render a compared view element.
203
     *
204
     * @param mixed $baseObject
205
     * @param mixed $compareObject
206
     *
207
     * @return string
208
     */
209
    public function renderViewElementCompare(
210
        Environment $environment,
211
        FieldDescriptionInterface $fieldDescription,
212
        $baseObject,
213
        $compareObject
214
    ) {
215
        $template = $this->getTemplate(
216
            $fieldDescription,
217
            '@SonataAdmin/CRUD/base_show_field.html.twig',
218
            $environment
219
        );
220
221
        try {
222
            $baseValue = $fieldDescription->getValue($baseObject);
223
        } catch (NoValueException $e) {
224
            $baseValue = null;
225
        }
226
227
        try {
228
            $compareValue = $fieldDescription->getValue($compareObject);
229
        } catch (NoValueException $e) {
230
            $compareValue = null;
231
        }
232
233
        $baseValueOutput = $template->render([
234
            'admin' => $fieldDescription->getAdmin(),
235
            'field_description' => $fieldDescription,
236
            'value' => $baseValue,
237
        ]);
238
239
        $compareValueOutput = $template->render([
240
            'field_description' => $fieldDescription,
241
            'admin' => $fieldDescription->getAdmin(),
242
            'value' => $compareValue,
243
        ]);
244
245
        // Compare the rendered output of both objects by using the (possibly) overridden field block
246
        $isDiff = $baseValueOutput !== $compareValueOutput;
247
248
        return $this->render($fieldDescription, $template, [
249
            'field_description' => $fieldDescription,
250
            'value' => $baseValue,
251
            'value_compare' => $compareValue,
252
            'is_diff' => $isDiff,
253
            'admin' => $fieldDescription->getAdmin(),
254
        ], $environment);
255
    }
256
257
    /**
258
     * @param mixed $element
259
     *
260
     * @throws \RuntimeException
261
     *
262
     * @return mixed
263
     */
264
    public function renderRelationElement($element, FieldDescriptionInterface $fieldDescription)
265
    {
266
        if (!is_object($element)) {
267
            return $element;
268
        }
269
270
        $propertyPath = $fieldDescription->getOption('associated_property');
271
272
        if (null === $propertyPath) {
273
            // For BC kept associated_tostring option behavior
274
            $method = $fieldDescription->getOption('associated_tostring');
275
276
            if ($method) {
277
                @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...
278
                    'Option "associated_tostring" is deprecated since version 2.3 and will be removed in 4.0. '
279
                    .'Use "associated_property" instead.',
280
                    E_USER_DEPRECATED
281
                );
282
            } else {
283
                $method = '__toString';
284
            }
285
286
            if (!method_exists($element, $method)) {
287
                throw new \RuntimeException(sprintf(
288
                    'You must define an `associated_property` option or '.
289
                    'create a `%s::__toString` method to the field option %s from service %s is ',
290
                    get_class($element),
291
                    $fieldDescription->getName(),
292
                    $fieldDescription->getAdmin()->getCode()
293
                ));
294
            }
295
296
            return call_user_func([$element, $method]);
297
        }
298
299
        if (is_callable($propertyPath)) {
300
            return $propertyPath($element);
301
        }
302
303
        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...
304
    }
305
306
    /**
307
     * Get the identifiers as a string that is safe to use in a url.
308
     *
309
     * @param object $model
310
     *
311
     * @return string string representation of the id that is safe to use in a url
312
     */
313
    public function getUrlsafeIdentifier($model, AdminInterface $admin = null)
314
    {
315
        if (null === $admin) {
316
            $admin = $this->pool->getAdminByClass(ClassUtils::getClass($model));
317
        }
318
319
        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...
320
    }
321
322
    /**
323
     * @param string[] $xEditableTypeMapping
324
     */
325
    public function setXEditableTypeMapping($xEditableTypeMapping): void
326
    {
327
        $this->xEditableTypeMapping = $xEditableTypeMapping;
328
    }
329
330
    /**
331
     * @return string|bool
332
     */
333
    public function getXEditableType($type)
334
    {
335
        return isset($this->xEditableTypeMapping[$type]) ? $this->xEditableTypeMapping[$type] : false;
336
    }
337
338
    /**
339
     * Return xEditable choices based on the field description choices options & catalogue options.
340
     * With the following choice options:
341
     *     ['Status1' => 'Alias1', 'Status2' => 'Alias2']
342
     * The method will return:
343
     *     [['value' => 'Status1', 'text' => 'Alias1'], ['value' => 'Status2', 'text' => 'Alias2']].
344
     *
345
     * @return array
346
     */
347
    public function getXEditableChoices(FieldDescriptionInterface $fieldDescription)
348
    {
349
        $choices = $fieldDescription->getOption('choices', []);
350
        $catalogue = $fieldDescription->getOption('catalogue');
351
        $xEditableChoices = [];
352
        if (!empty($choices)) {
353
            reset($choices);
354
            $first = current($choices);
355
            // the choices are already in the right format
356
            if (is_array($first) && array_key_exists('value', $first) && array_key_exists('text', $first)) {
357
                $xEditableChoices = $choices;
358
            } else {
359
                foreach ($choices as $value => $text) {
360
                    if ($catalogue) {
361
                        if (null !== $this->translator) {
362
                            $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...
363
                        // NEXT_MAJOR: Remove this check
364
                        } elseif (method_exists($fieldDescription->getAdmin(), 'trans')) {
365
                            $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...
366
                        }
367
                    }
368
369
                    $xEditableChoices[] = [
370
                        'value' => $value,
371
                        'text' => $text,
372
                    ];
373
                }
374
            }
375
        }
376
377
        if (false === $fieldDescription->getOption('required', true)
378
            && false === $fieldDescription->getOption('multiple', false)
379
        ) {
380
            $xEditableChoices = array_merge([[
381
                'value' => '',
382
                'text' => '',
383
            ]], $xEditableChoices);
384
        }
385
386
        return $xEditableChoices;
387
    }
388
389
    /*
390
     * Returns a canonicalized locale for "moment" NPM library,
391
     * or `null` if the locale's language is "en", which doesn't require localization.
392
     *
393
     * @return null|string
394
     */
395
    public function getCanonicalizedLocaleForMoment(array $context)
396
    {
397
        $locale = strtolower(str_replace('_', '-', $context['app']->getRequest()->getLocale()));
398
399
        // "en" language doesn't require localization.
400
        if (('en' === $lang = substr($locale, 0, 2)) && !in_array($locale, ['en-au', 'en-ca', 'en-gb', 'en-ie', 'en-nz'], true)) {
401
            return null;
402
        }
403
404
        if ('es' === $lang && !in_array($locale, ['es', 'es-do'], true)) {
405
            // `moment: ^2.8` only ships "es" and "es-do" locales for "es" language
406
            $locale = 'es';
407
        } elseif ('nl' === $lang && !in_array($locale, ['nl', 'nl-be'], true)) {
408
            // `moment: ^2.8` only ships "nl" and "nl-be" locales for "nl" language
409
            $locale = 'nl';
410
        }
411
412
        // @todo: there are more locales which are not supported by moment and they need to be translated/normalized/canonicalized here
413
414
        return $locale;
415
    }
416
417
    /**
418
     * Returns a canonicalized locale for "select2" NPM library,
419
     * or `null` if the locale's language is "en", which doesn't require localization.
420
     *
421
     * @return null|string
422
     */
423
    public function getCanonicalizedLocaleForSelect2(array $context)
424
    {
425
        $locale = str_replace('_', '-', $context['app']->getRequest()->getLocale());
426
427
        // "en" language doesn't require localization.
428
        if ('en' === $lang = substr($locale, 0, 2)) {
429
            return null;
430
        }
431
432
        switch ($locale) {
433
            case 'pt':
434
                $locale = 'pt-PT';
435
                break;
436
            case 'ug':
437
                $locale = 'ug-CN';
438
                break;
439
            case 'zh':
440
                $locale = 'zh-CN';
441
                break;
442
            default:
443
                if (!in_array($locale, ['pt-BR', 'pt-PT', 'ug-CN', 'zh-CN', 'zh-TW'], true)) {
444
                    $locale = $lang;
445
                }
446
        }
447
448
        return $locale;
449
    }
450
451
    /**
452
     * return the value related to FieldDescription, if the associated object does no
453
     * exists => a temporary one is created.
454
     *
455
     * @param object $object
456
     *
457
     * @throws \RuntimeException
458
     *
459
     * @return mixed
460
     */
461
    private function getValueFromFieldDescription(
462
        $object,
463
        FieldDescriptionInterface $fieldDescription,
464
        array $params = []
465
    ) {
466
        if (isset($params['loop']) && $object instanceof \ArrayAccess) {
467
            throw new \RuntimeException('remove the loop requirement');
468
        }
469
470
        $value = null;
471
472
        try {
473
            $value = $fieldDescription->getValue($object);
474
        } catch (NoValueException $e) {
475
            if ($fieldDescription->getAssociationAdmin()) {
476
                $value = $fieldDescription->getAssociationAdmin()->getNewInstance();
477
            }
478
        }
479
480
        return $value;
481
    }
482
483
    /**
484
     * Get template.
485
     *
486
     * @param string $defaultTemplate
487
     *
488
     * @return TemplateWrapper
489
     */
490
    private function getTemplate(
491
        FieldDescriptionInterface $fieldDescription,
492
        $defaultTemplate,
493
        Environment $environment
494
    ) {
495
        $templateName = $fieldDescription->getTemplate() ?: $defaultTemplate;
496
497
        try {
498
            $template = $environment->load($templateName);
499
        } 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...
500
            @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...
501
                'Relying on default template loading on field template loading exception '.
502
                'is deprecated since 3.1 and will be removed in 4.0. '.
503
                'A \Twig_Error_Loader exception will be thrown instead',
504
                E_USER_DEPRECATED
505
            );
506
            $template = $environment->load($defaultTemplate);
507
508
            if (null !== $this->logger) {
509
                $this->logger->warning(sprintf(
510
                    'An error occured trying to load the template "%s" for the field "%s", '.
511
                    'the default template "%s" was used instead.',
512
                    $templateName,
513
                    $fieldDescription->getFieldName(),
514
                    $defaultTemplate
515
                ), ['exception' => $e]);
516
            }
517
        }
518
519
        return $template;
520
    }
521
522
    /**
523
     * @return string
524
     */
525
    private function render(
526
        FieldDescriptionInterface $fieldDescription,
527
        TemplateWrapper $template,
528
        array $parameters,
529
        Environment $environment
530
    ) {
531
        $content = $template->render($parameters);
532
533
        if ($environment->isDebug()) {
534
            $commentTemplate = <<<'EOT'
535
536
<!-- START
537
    fieldName: %s
538
    template: %s
539
    compiled template: %s
540
    -->
541
    %s
542
<!-- END - fieldName: %s -->
543
EOT;
544
545
            return sprintf(
546
                $commentTemplate,
547
                $fieldDescription->getFieldName(),
548
                $fieldDescription->getTemplate(),
549
                $template->getSourceContext()->getName(),
550
                $content,
551
                $fieldDescription->getFieldName()
552
            );
553
        }
554
555
        return $content;
556
    }
557
558
    /**
559
     * @param string $adminCode
560
     *
561
     * @throws ServiceCircularReferenceException
562
     * @throws ServiceNotFoundException
563
     *
564
     * @return TemplateRegistryInterface
565
     */
566
    private function getTemplateRegistry($adminCode)
567
    {
568
        $serviceId = $adminCode.'.template_registry';
569
        $templateRegistry = $this->templateRegistries->get($serviceId);
570
571
        if ($templateRegistry instanceof TemplateRegistryInterface) {
572
            return $templateRegistry;
573
        }
574
575
        throw new ServiceNotFoundException($serviceId);
576
    }
577
}
578