Completed
Push — 3.x ( ecae9a...388af4 )
by Jordi Sala
03:54
created

SonataAdminExtension::output()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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