Completed
Push — master ( ce5dd0...a358bf )
by
unknown
04:17
created

CRUDController::getSelectedTab()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 57 and the first side effect is on line 51.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
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\Controller;
15
16
use Doctrine\Common\Inflector\Inflector;
17
use Psr\Log\LoggerInterface;
18
use Psr\Log\NullLogger;
19
use Sonata\AdminBundle\Admin\AdminInterface;
20
use Sonata\AdminBundle\Admin\FieldDescriptionCollection;
21
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
22
use Sonata\AdminBundle\Exception\LockException;
23
use Sonata\AdminBundle\Exception\ModelManagerException;
24
use Sonata\AdminBundle\Templating\TemplateRegistryInterface;
25
use Sonata\AdminBundle\Util\AdminObjectAclData;
26
use Sonata\AdminBundle\Util\AdminObjectAclManipulator;
27
use Symfony\Bridge\Twig\AppVariable;
28
use Symfony\Bridge\Twig\Command\DebugCommand;
29
use Symfony\Bridge\Twig\Extension\FormExtension;
30
use Symfony\Bridge\Twig\Form\TwigRenderer;
31
use Symfony\Bundle\FrameworkBundle\Controller\ControllerTrait;
32
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
33
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
34
use Symfony\Component\DependencyInjection\ContainerInterface;
35
use Symfony\Component\Form\FormRenderer;
36
use Symfony\Component\Form\FormView;
37
use Symfony\Component\HttpFoundation\JsonResponse;
38
use Symfony\Component\HttpFoundation\RedirectResponse;
39
use Symfony\Component\HttpFoundation\Request;
40
use Symfony\Component\HttpFoundation\Response;
41
use Symfony\Component\HttpKernel\Exception\HttpException;
42
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
43
use Symfony\Component\PropertyAccess\PropertyAccess;
44
use Symfony\Component\PropertyAccess\PropertyPath;
45
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
46
use Symfony\Component\Security\Csrf\CsrfToken;
47
48
// BC for Symfony < 3.3 where this trait does not exist
49
// NEXT_MAJOR: Remove the polyfill and inherit from \Symfony\Bundle\FrameworkBundle\Controller\Controller again
50
if (!trait_exists(ControllerTrait::class)) {
51
    require_once __DIR__.'/PolyfillControllerTrait.php';
52
}
53
54
/**
55
 * @author Thomas Rabaix <[email protected]>
56
 */
57
class CRUDController implements ContainerAwareInterface
58
{
59
    // NEXT_MAJOR: Don't use these traits anymore (inherit from Controller instead)
60
    use ControllerTrait, ContainerAwareTrait {
61
        ControllerTrait::render as originalRender;
62
    }
63
64
    /**
65
     * The related Admin class.
66
     *
67
     * @var AdminInterface
68
     */
69
    protected $admin;
70
71
    /**
72
     * The template registry of the related Admin class.
73
     *
74
     * @var TemplateRegistryInterface
75
     */
76
    private $templateRegistry;
77
78
    // BC for Symfony 3.3 where ControllerTrait exists but does not contain get() and has() methods.
79
    public function __call($method, $arguments)
80
    {
81
        if (\in_array($method, ['get', 'has'], true)) {
82
            return $this->container->$method(...$arguments);
83
        }
84
85
        if (method_exists($this, 'proxyToControllerClass')) {
86
            return $this->proxyToControllerClass($method, $arguments);
0 ignored issues
show
Documentation Bug introduced by
The method proxyToControllerClass does not exist on object<Sonata\AdminBundl...troller\CRUDController>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
87
        }
88
89
        throw new \LogicException('Call to undefined method '.__CLASS__.'::'.$method);
90
    }
91
92
    public function setContainer(ContainerInterface $container = null): void
93
    {
94
        $this->container = $container;
95
96
        $this->configure();
97
    }
98
99
    /**
100
     * NEXT_MAJOR: Remove this method.
101
     *
102
     * @see renderWithExtraParams()
103
     *
104
     * @param string $view       The view name
105
     * @param array  $parameters An array of parameters to pass to the view
106
     *
107
     * @return Response A Response instance
108
     *
109
     * @deprecated since version 3.27, to be removed in 4.0. Use Sonata\AdminBundle\Controller\CRUDController::renderWithExtraParams() instead.
110
     */
111
    public function render($view, array $parameters = [], Response $response = null)
112
    {
113
        @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...
114
            'Method '.__CLASS__.'::render has been renamed to '.__CLASS__.'::renderWithExtraParams.',
115
            E_USER_DEPRECATED
116
        );
117
118
        return $this->renderWithExtraParams($view, $parameters, $response);
119
    }
120
121
    /**
122
     * Renders a view while passing mandatory parameters on to the template.
123
     *
124
     * @param string $view The view name
125
     *
126
     * @return Response A Response instance
127
     */
128
    public function renderWithExtraParams($view, array $parameters = [], Response $response = null)
129
    {
130
        if (!$this->isXmlHttpRequest()) {
131
            $parameters['breadcrumbs_builder'] = $this->get('sonata.admin.breadcrumbs_builder');
132
        }
133
        $parameters['admin'] = $parameters['admin'] ??
134
            $this->admin;
135
136
        $parameters['base_template'] = $parameters['base_template'] ??
137
            $this->getBaseTemplate();
138
139
        $parameters['admin_pool'] = $this->get('sonata.admin.pool');
140
141
        //NEXT_MAJOR: Remove method alias and use $this->render() directly.
142
        return $this->originalRender($view, $parameters, $response);
143
    }
144
145
    /**
146
     * List action.
147
     *
148
     * @throws AccessDeniedException If access is not granted
149
     *
150
     * @return Response
151
     */
152
    public function listAction()
153
    {
154
        $request = $this->getRequest();
155
156
        $this->admin->checkAccess('list');
157
158
        $preResponse = $this->preList($request);
159
        if (null !== $preResponse) {
160
            return $preResponse;
161
        }
162
163
        if ($listMode = $request->get('_list_mode')) {
164
            $this->admin->setListMode($listMode);
165
        }
166
167
        $datagrid = $this->admin->getDatagrid();
168
        $formView = $datagrid->getForm()->createView();
169
170
        // set the theme for the current Admin Form
171
        $this->setFormTheme($formView, $this->admin->getFilterTheme());
172
173
        // NEXT_MAJOR: Remove this line and use commented line below it instead
174
        $template = $this->admin->getTemplate('list');
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...
175
        // $template = $this->templateRegistry->getTemplate('list');
176
177
        return $this->renderWithExtraParams($template, [
178
            'action' => 'list',
179
            'form' => $formView,
180
            'datagrid' => $datagrid,
181
            'csrf_token' => $this->getCsrfToken('sonata.batch'),
182
            'export_formats' => $this->has('sonata.admin.admin_exporter') ?
183
                $this->get('sonata.admin.admin_exporter')->getAvailableFormats($this->admin) :
184
                $this->admin->getExportFormats(),
185
        ], null);
186
    }
187
188
    /**
189
     * Execute a batch delete.
190
     *
191
     * @throws AccessDeniedException If access is not granted
192
     *
193
     * @return RedirectResponse
194
     */
195
    public function batchActionDelete(ProxyQueryInterface $query)
196
    {
197
        $this->admin->checkAccess('batchDelete');
198
199
        $modelManager = $this->admin->getModelManager();
200
201
        try {
202
            $modelManager->batchDelete($this->admin->getClass(), $query);
203
            $this->addFlash(
204
                'sonata_flash_success',
205
                $this->trans('flash_batch_delete_success', [], 'SonataAdminBundle')
206
            );
207
        } catch (ModelManagerException $e) {
208
            $this->handleModelManagerException($e);
209
            $this->addFlash(
210
                'sonata_flash_error',
211
                $this->trans('flash_batch_delete_error', [], 'SonataAdminBundle')
212
            );
213
        }
214
215
        return $this->redirectToList();
216
    }
217
218
    /**
219
     * Delete action.
220
     *
221
     * @param int|string|null $id
222
     *
223
     * @throws NotFoundHttpException If the object does not exist
224
     * @throws AccessDeniedException If access is not granted
225
     *
226
     * @return Response|RedirectResponse
227
     */
228
    public function deleteAction($id)
0 ignored issues
show
Unused Code introduced by
The parameter $id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
229
    {
230
        $request = $this->getRequest();
231
        $id = $request->get($this->admin->getIdParameter());
232
        $object = $this->admin->getObject($id);
233
234
        if (!$object) {
235
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
236
        }
237
238
        $this->checkParentChildAssociation($request, $object);
239
240
        $this->admin->checkAccess('delete', $object);
241
242
        $preResponse = $this->preDelete($request, $object);
243
        if (null !== $preResponse) {
244
            return $preResponse;
245
        }
246
247
        if ('DELETE' === $this->getRestMethod()) {
248
            // check the csrf token
249
            $this->validateCsrfToken('sonata.delete');
250
251
            $objectName = $this->admin->toString($object);
252
253
            try {
254
                $this->admin->delete($object);
255
256
                if ($this->isXmlHttpRequest()) {
257
                    return $this->renderJson(['result' => 'ok'], 200, []);
258
                }
259
260
                $this->addFlash(
261
                    'sonata_flash_success',
262
                    $this->trans(
263
                        'flash_delete_success',
264
                        ['%name%' => $this->escapeHtml($objectName)],
265
                        'SonataAdminBundle'
266
                    )
267
                );
268
            } catch (ModelManagerException $e) {
269
                $this->handleModelManagerException($e);
270
271
                if ($this->isXmlHttpRequest()) {
272
                    return $this->renderJson(['result' => 'error'], 200, []);
273
                }
274
275
                $this->addFlash(
276
                    'sonata_flash_error',
277
                    $this->trans(
278
                        'flash_delete_error',
279
                        ['%name%' => $this->escapeHtml($objectName)],
280
                        'SonataAdminBundle'
281
                    )
282
                );
283
            }
284
285
            return $this->redirectTo($object);
286
        }
287
288
        // NEXT_MAJOR: Remove this line and use commented line below it instead
289
        $template = $this->admin->getTemplate('delete');
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...
290
        // $template = $this->templateRegistry->getTemplate('delete');
291
292
        return $this->renderWithExtraParams($template, [
293
            'object' => $object,
294
            'action' => 'delete',
295
            'csrf_token' => $this->getCsrfToken('sonata.delete'),
296
        ], null);
297
    }
298
299
    /**
300
     * Edit action.
301
     *
302
     * @param int|string|null $id
303
     *
304
     * @throws NotFoundHttpException If the object does not exist
305
     * @throws \RuntimeException     If no editable field is defined
306
     * @throws AccessDeniedException If access is not granted
307
     *
308
     * @return Response|RedirectResponse
309
     */
310
    public function editAction($id = null)
0 ignored issues
show
Unused Code introduced by
The parameter $id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
311
    {
312
        $request = $this->getRequest();
313
        // the key used to lookup the template
314
        $templateKey = 'edit';
315
316
        $id = $request->get($this->admin->getIdParameter());
317
        $existingObject = $this->admin->getObject($id);
318
319
        if (!$existingObject) {
320
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
321
        }
322
323
        $this->checkParentChildAssociation($request, $existingObject);
324
325
        $this->admin->checkAccess('edit', $existingObject);
326
327
        $preResponse = $this->preEdit($request, $existingObject);
328
        if (null !== $preResponse) {
329
            return $preResponse;
330
        }
331
332
        $this->admin->setSubject($existingObject);
333
        $objectId = $this->admin->getNormalizedIdentifier($existingObject);
334
335
        $form = $this->admin->getForm();
336
337
        if (!\is_array($fields = $form->all()) || 0 === \count($fields)) {
338
            throw new \RuntimeException(
339
                'No editable field defined. Did you forget to implement the "configureFormFields" method?'
340
            );
341
        }
342
343
        $form->setData($existingObject);
344
        $form->handleRequest($request);
345
346
        if ($form->isSubmitted()) {
347
            $isFormValid = $form->isValid();
348
349
            // persist if the form was valid and if in preview mode the preview was approved
350
            if ($isFormValid && (!$this->isInPreviewMode() || $this->isPreviewApproved())) {
351
                $submittedObject = $form->getData();
352
                $this->admin->setSubject($submittedObject);
353
354
                try {
355
                    $existingObject = $this->admin->update($submittedObject);
356
357
                    if ($this->isXmlHttpRequest()) {
358
                        return $this->renderJson([
359
                            'result' => 'ok',
360
                            'objectId' => $objectId,
361
                            'objectName' => $this->escapeHtml($this->admin->toString($existingObject)),
362
                        ], 200, []);
363
                    }
364
365
                    $this->addFlash(
366
                        'sonata_flash_success',
367
                        $this->trans(
368
                            'flash_edit_success',
369
                            ['%name%' => $this->escapeHtml($this->admin->toString($existingObject))],
370
                            'SonataAdminBundle'
371
                        )
372
                    );
373
374
                    // redirect to edit mode
375
                    return $this->redirectTo($existingObject);
376
                } catch (ModelManagerException $e) {
377
                    $this->handleModelManagerException($e);
378
379
                    $isFormValid = false;
380
                } catch (LockException $e) {
381
                    $this->addFlash('sonata_flash_error', $this->trans('flash_lock_error', [
382
                        '%name%' => $this->escapeHtml($this->admin->toString($existingObject)),
383
                        '%link_start%' => '<a href="'.$this->admin->generateObjectUrl('edit', $existingObject).'">',
384
                        '%link_end%' => '</a>',
385
                    ], 'SonataAdminBundle'));
386
                }
387
            }
388
389
            // show an error message if the form failed validation
390
            if (!$isFormValid) {
391
                if (!$this->isXmlHttpRequest()) {
392
                    $this->addFlash(
393
                        'sonata_flash_error',
394
                        $this->trans(
395
                            'flash_edit_error',
396
                            ['%name%' => $this->escapeHtml($this->admin->toString($existingObject))],
397
                            'SonataAdminBundle'
398
                        )
399
                    );
400
                }
401
            } elseif ($this->isPreviewRequested()) {
402
                // enable the preview template if the form was valid and preview was requested
403
                $templateKey = 'preview';
404
                $this->admin->getShow();
405
            }
406
        }
407
408
        $formView = $form->createView();
409
        // set the theme for the current Admin Form
410
        $this->setFormTheme($formView, $this->admin->getFormTheme());
411
412
        // NEXT_MAJOR: Remove this line and use commented line below it instead
413
        $template = $this->admin->getTemplate($templateKey);
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...
414
        // $template = $this->templateRegistry->getTemplate($templateKey);
415
416
        return $this->renderWithExtraParams($template, [
417
            'action' => 'edit',
418
            'form' => $formView,
419
            'object' => $existingObject,
420
            'objectId' => $objectId,
421
        ], null);
422
    }
423
424
    /**
425
     * Batch action.
426
     *
427
     * @throws NotFoundHttpException If the HTTP method is not POST
428
     * @throws \RuntimeException     If the batch action is not defined
429
     *
430
     * @return Response|RedirectResponse
431
     */
432
    public function batchAction()
433
    {
434
        $request = $this->getRequest();
435
        $restMethod = $this->getRestMethod();
436
437
        if ('POST' !== $restMethod) {
438
            throw $this->createNotFoundException(sprintf('Invalid request type "%s", POST expected', $restMethod));
439
        }
440
441
        // check the csrf token
442
        $this->validateCsrfToken('sonata.batch');
443
444
        $confirmation = $request->get('confirmation', false);
445
446
        if ($data = json_decode($request->get('data', ''), true)) {
447
            $action = $data['action'];
448
            $idx = $data['idx'];
449
            $allElements = $data['all_elements'];
450
            $request->request->replace(array_merge($request->request->all(), $data));
451
        } else {
452
            $request->request->set('idx', $request->get('idx', []));
453
            $request->request->set('all_elements', $request->get('all_elements', false));
454
455
            $action = $request->get('action');
456
            $idx = $request->get('idx');
457
            $allElements = $request->get('all_elements');
458
            $data = $request->request->all();
459
460
            unset($data['_sonata_csrf_token']);
461
        }
462
463
        $batchActions = $this->admin->getBatchActions();
464
        if (!\array_key_exists($action, $batchActions)) {
465
            throw new \RuntimeException(sprintf('The `%s` batch action is not defined', $action));
466
        }
467
468
        $camelizedAction = Inflector::classify($action);
469
        $isRelevantAction = sprintf('batchAction%sIsRelevant', ucfirst($camelizedAction));
470
471
        if (method_exists($this, $isRelevantAction)) {
472
            $nonRelevantMessage = $this->$isRelevantAction($idx, $allElements, $request);
473
        } else {
474
            $nonRelevantMessage = 0 !== \count($idx) || $allElements; // at least one item is selected
475
        }
476
477
        if (!$nonRelevantMessage) { // default non relevant message (if false of null)
478
            $nonRelevantMessage = 'flash_batch_empty';
479
        }
480
481
        $datagrid = $this->admin->getDatagrid();
482
        $datagrid->buildPager();
483
484
        if (true !== $nonRelevantMessage) {
485
            $this->addFlash(
486
                'sonata_flash_info',
487
                $this->trans($nonRelevantMessage, [], 'SonataAdminBundle')
488
            );
489
490
            return $this->redirectToList();
491
        }
492
493
        $askConfirmation = $batchActions[$action]['ask_confirmation'] ??
494
            true;
495
496
        if ($askConfirmation && 'ok' !== $confirmation) {
497
            $actionLabel = $batchActions[$action]['label'];
498
            $batchTranslationDomain = $batchActions[$action]['translation_domain'] ??
499
                $this->admin->getTranslationDomain();
500
501
            $formView = $datagrid->getForm()->createView();
502
            $this->setFormTheme($formView, $this->admin->getFilterTheme());
503
504
            // NEXT_MAJOR: Remove these lines and use commented lines below them instead
505
            $template = !empty($batchActions[$action]['template']) ?
506
                $batchActions[$action]['template'] :
507
                $this->admin->getTemplate('batch_confirmation');
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...
508
            // $template = !empty($batchActions[$action]['template']) ?
509
            //     $batchActions[$action]['template'] :
510
            //     $this->templateRegistry->getTemplate('batch_confirmation');
511
512
            return $this->renderWithExtraParams($template, [
513
                'action' => 'list',
514
                'action_label' => $actionLabel,
515
                'batch_translation_domain' => $batchTranslationDomain,
516
                'datagrid' => $datagrid,
517
                'form' => $formView,
518
                'data' => $data,
519
                'csrf_token' => $this->getCsrfToken('sonata.batch'),
520
            ], null);
521
        }
522
523
        // execute the action, batchActionXxxxx
524
        $finalAction = sprintf('batchAction%s', $camelizedAction);
525
        if (!method_exists($this, $finalAction)) {
526
            throw new \RuntimeException(sprintf('A `%s::%s` method must be callable', \get_class($this), $finalAction));
527
        }
528
529
        $query = $datagrid->getQuery();
530
531
        $query->setFirstResult(null);
532
        $query->setMaxResults(null);
533
534
        $this->admin->preBatchAction($action, $query, $idx, $allElements);
535
536
        if (\count($idx) > 0) {
537
            $this->admin->getModelManager()->addIdentifiersToQuery($this->admin->getClass(), $query, $idx);
538
        } elseif (!$allElements) {
539
            $this->addFlash(
540
                'sonata_flash_info',
541
                $this->trans('flash_batch_no_elements_processed', [], 'SonataAdminBundle')
542
            );
543
544
            return $this->redirectToList();
545
        }
546
547
        return $this->$finalAction($query, $request);
548
    }
549
550
    /**
551
     * Create action.
552
     *
553
     * @throws AccessDeniedException If access is not granted
554
     * @throws \RuntimeException     If no editable field is defined
555
     *
556
     * @return Response
557
     */
558
    public function createAction()
559
    {
560
        $request = $this->getRequest();
561
        // the key used to lookup the template
562
        $templateKey = 'edit';
563
564
        $this->admin->checkAccess('create');
565
566
        $class = new \ReflectionClass($this->admin->hasActiveSubClass() ? $this->admin->getActiveSubClass() : $this->admin->getClass());
567
568
        if ($class->isAbstract()) {
569
            return $this->renderWithExtraParams(
570
                '@SonataAdmin/CRUD/select_subclass.html.twig',
571
                [
572
                    'base_template' => $this->getBaseTemplate(),
573
                    'admin' => $this->admin,
574
                    'action' => 'create',
575
                ],
576
                null
577
            );
578
        }
579
580
        $newObject = $this->admin->getNewInstance();
581
582
        $preResponse = $this->preCreate($request, $newObject);
583
        if (null !== $preResponse) {
584
            return $preResponse;
585
        }
586
587
        $this->admin->setSubject($newObject);
588
589
        $form = $this->admin->getForm();
590
591
        if (!\is_array($fields = $form->all()) || 0 === \count($fields)) {
592
            throw new \RuntimeException(
593
                'No editable field defined. Did you forget to implement the "configureFormFields" method?'
594
            );
595
        }
596
597
        $form->setData($newObject);
598
        $form->handleRequest($request);
599
600
        if ($form->isSubmitted()) {
601
            $isFormValid = $form->isValid();
602
603
            // persist if the form was valid and if in preview mode the preview was approved
604
            if ($isFormValid && (!$this->isInPreviewMode() || $this->isPreviewApproved())) {
605
                $submittedObject = $form->getData();
606
                $this->admin->setSubject($submittedObject);
607
                $this->admin->checkAccess('create', $submittedObject);
608
609
                try {
610
                    $newObject = $this->admin->create($submittedObject);
611
612
                    if ($this->isXmlHttpRequest()) {
613
                        return $this->renderJson([
614
                            'result' => 'ok',
615
                            'objectId' => $this->admin->getNormalizedIdentifier($newObject),
616
                            'objectName' => $this->escapeHtml($this->admin->toString($newObject)),
617
                        ], 200, []);
618
                    }
619
620
                    $this->addFlash(
621
                        'sonata_flash_success',
622
                        $this->trans(
623
                            'flash_create_success',
624
                            ['%name%' => $this->escapeHtml($this->admin->toString($newObject))],
625
                            'SonataAdminBundle'
626
                        )
627
                    );
628
629
                    // redirect to edit mode
630
                    return $this->redirectTo($newObject);
631
                } catch (ModelManagerException $e) {
632
                    $this->handleModelManagerException($e);
633
634
                    $isFormValid = false;
635
                }
636
            }
637
638
            // show an error message if the form failed validation
639
            if (!$isFormValid) {
640
                if (!$this->isXmlHttpRequest()) {
641
                    $this->addFlash(
642
                        'sonata_flash_error',
643
                        $this->trans(
644
                            'flash_create_error',
645
                            ['%name%' => $this->escapeHtml($this->admin->toString($newObject))],
646
                            'SonataAdminBundle'
647
                        )
648
                    );
649
                }
650
            } elseif ($this->isPreviewRequested()) {
651
                // pick the preview template if the form was valid and preview was requested
652
                $templateKey = 'preview';
653
                $this->admin->getShow();
654
            }
655
        }
656
657
        $formView = $form->createView();
658
        // set the theme for the current Admin Form
659
        $this->setFormTheme($formView, $this->admin->getFormTheme());
660
661
        // NEXT_MAJOR: Remove this line and use commented line below it instead
662
        $template = $this->admin->getTemplate($templateKey);
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...
663
        // $template = $this->templateRegistry->getTemplate($templateKey);
664
665
        return $this->renderWithExtraParams($template, [
666
            'action' => 'create',
667
            'form' => $formView,
668
            'object' => $newObject,
669
            'objectId' => null,
670
        ], null);
671
    }
672
673
    /**
674
     * Show action.
675
     *
676
     * @param int|string|null $id
677
     *
678
     * @throws NotFoundHttpException If the object does not exist
679
     * @throws AccessDeniedException If access is not granted
680
     *
681
     * @return Response
682
     */
683
    public function showAction($id = null)
0 ignored issues
show
Unused Code introduced by
The parameter $id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
684
    {
685
        $request = $this->getRequest();
686
        $id = $request->get($this->admin->getIdParameter());
687
688
        $object = $this->admin->getObject($id);
689
690
        if (!$object) {
691
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
692
        }
693
694
        $this->checkParentChildAssociation($request, $object);
695
696
        $this->admin->checkAccess('show', $object);
697
698
        $preResponse = $this->preShow($request, $object);
699
        if (null !== $preResponse) {
700
            return $preResponse;
701
        }
702
703
        $this->admin->setSubject($object);
704
705
        $fields = $this->admin->getShow();
706
        \assert($fields instanceof FieldDescriptionCollection);
707
708
        // NEXT_MAJOR: replace deprecation with exception
709
        if (!\is_array($fields->getElements()) || 0 === $fields->count()) {
710
            @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...
711
                'Calling this method without implementing "configureShowFields"'
712
                .' is not supported since 3.40.0'
713
                .' and will no longer be possible in 4.0',
714
                E_USER_DEPRECATED
715
            );
716
        }
717
718
        // NEXT_MAJOR: Remove this line and use commented line below it instead
719
        $template = $this->admin->getTemplate('show');
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...
720
        //$template = $this->templateRegistry->getTemplate('show');
721
722
        return $this->renderWithExtraParams($template, [
723
            'action' => 'show',
724
            'object' => $object,
725
            'elements' => $fields,
726
        ], null);
727
    }
728
729
    /**
730
     * Show history revisions for object.
731
     *
732
     * @param int|string|null $id
733
     *
734
     * @throws AccessDeniedException If access is not granted
735
     * @throws NotFoundHttpException If the object does not exist or the audit reader is not available
736
     *
737
     * @return Response
738
     */
739
    public function historyAction($id = null)
0 ignored issues
show
Unused Code introduced by
The parameter $id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
740
    {
741
        $request = $this->getRequest();
742
        $id = $request->get($this->admin->getIdParameter());
743
744
        $object = $this->admin->getObject($id);
745
746
        if (!$object) {
747
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
748
        }
749
750
        $this->admin->checkAccess('history', $object);
751
752
        $manager = $this->get('sonata.admin.audit.manager');
753
754
        if (!$manager->hasReader($this->admin->getClass())) {
755
            throw $this->createNotFoundException(
756
                sprintf(
757
                    'unable to find the audit reader for class : %s',
758
                    $this->admin->getClass()
759
                )
760
            );
761
        }
762
763
        $reader = $manager->getReader($this->admin->getClass());
764
765
        $revisions = $reader->findRevisions($this->admin->getClass(), $id);
766
767
        // NEXT_MAJOR: Remove this line and use commented line below it instead
768
        $template = $this->admin->getTemplate('history');
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...
769
        // $template = $this->templateRegistry->getTemplate('history');
770
771
        return $this->renderWithExtraParams($template, [
772
            'action' => 'history',
773
            'object' => $object,
774
            'revisions' => $revisions,
775
            'currentRevision' => $revisions ? current($revisions) : false,
776
        ], null);
777
    }
778
779
    /**
780
     * View history revision of object.
781
     *
782
     * @param int|string|null $id
783
     * @param string|null     $revision
784
     *
785
     * @throws AccessDeniedException If access is not granted
786
     * @throws NotFoundHttpException If the object or revision does not exist or the audit reader is not available
787
     *
788
     * @return Response
789
     */
790
    public function historyViewRevisionAction($id = null, $revision = null)
0 ignored issues
show
Unused Code introduced by
The parameter $id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
791
    {
792
        $request = $this->getRequest();
793
        $id = $request->get($this->admin->getIdParameter());
794
795
        $object = $this->admin->getObject($id);
796
797
        if (!$object) {
798
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
799
        }
800
801
        $this->admin->checkAccess('historyViewRevision', $object);
802
803
        $manager = $this->get('sonata.admin.audit.manager');
804
805
        if (!$manager->hasReader($this->admin->getClass())) {
806
            throw $this->createNotFoundException(
807
                sprintf(
808
                    'unable to find the audit reader for class : %s',
809
                    $this->admin->getClass()
810
                )
811
            );
812
        }
813
814
        $reader = $manager->getReader($this->admin->getClass());
815
816
        // retrieve the revisioned object
817
        $object = $reader->find($this->admin->getClass(), $id, $revision);
818
819
        if (!$object) {
820
            throw $this->createNotFoundException(
821
                sprintf(
822
                    'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
823
                    $id,
824
                    $revision,
825
                    $this->admin->getClass()
826
                )
827
            );
828
        }
829
830
        $this->admin->setSubject($object);
831
832
        // NEXT_MAJOR: Remove this line and use commented line below it instead
833
        $template = $this->admin->getTemplate('show');
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...
834
        // $template = $this->templateRegistry->getTemplate('show');
835
836
        return $this->renderWithExtraParams($template, [
837
            'action' => 'show',
838
            'object' => $object,
839
            'elements' => $this->admin->getShow(),
840
        ], null);
841
    }
842
843
    /**
844
     * Compare history revisions of object.
845
     *
846
     * @param int|string|null $id
847
     * @param int|string|null $base_revision
848
     * @param int|string|null $compare_revision
849
     *
850
     * @throws AccessDeniedException If access is not granted
851
     * @throws NotFoundHttpException If the object or revision does not exist or the audit reader is not available
852
     *
853
     * @return Response
854
     */
855
    public function historyCompareRevisionsAction($id = null, $base_revision = null, $compare_revision = null)
0 ignored issues
show
Unused Code introduced by
The parameter $id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
856
    {
857
        $request = $this->getRequest();
858
859
        $this->admin->checkAccess('historyCompareRevisions');
860
861
        $id = $request->get($this->admin->getIdParameter());
862
863
        $object = $this->admin->getObject($id);
864
865
        if (!$object) {
866
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
867
        }
868
869
        $manager = $this->get('sonata.admin.audit.manager');
870
871
        if (!$manager->hasReader($this->admin->getClass())) {
872
            throw $this->createNotFoundException(
873
                sprintf(
874
                    'unable to find the audit reader for class : %s',
875
                    $this->admin->getClass()
876
                )
877
            );
878
        }
879
880
        $reader = $manager->getReader($this->admin->getClass());
881
882
        // retrieve the base revision
883
        $base_object = $reader->find($this->admin->getClass(), $id, $base_revision);
884
        if (!$base_object) {
885
            throw $this->createNotFoundException(
886
                sprintf(
887
                    'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
888
                    $id,
889
                    $base_revision,
890
                    $this->admin->getClass()
891
                )
892
            );
893
        }
894
895
        // retrieve the compare revision
896
        $compare_object = $reader->find($this->admin->getClass(), $id, $compare_revision);
897
        if (!$compare_object) {
898
            throw $this->createNotFoundException(
899
                sprintf(
900
                    'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
901
                    $id,
902
                    $compare_revision,
903
                    $this->admin->getClass()
904
                )
905
            );
906
        }
907
908
        $this->admin->setSubject($base_object);
909
910
        // NEXT_MAJOR: Remove this line and use commented line below it instead
911
        $template = $this->admin->getTemplate('show_compare');
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...
912
        // $template = $this->templateRegistry->getTemplate('show_compare');
913
914
        return $this->renderWithExtraParams($template, [
915
            'action' => 'show',
916
            'object' => $base_object,
917
            'object_compare' => $compare_object,
918
            'elements' => $this->admin->getShow(),
919
        ], null);
920
    }
921
922
    /**
923
     * Export data to specified format.
924
     *
925
     * @throws AccessDeniedException If access is not granted
926
     * @throws \RuntimeException     If the export format is invalid
927
     *
928
     * @return Response
929
     */
930
    public function exportAction(Request $request)
931
    {
932
        $this->admin->checkAccess('export');
933
934
        $format = $request->get('format');
935
936
        // NEXT_MAJOR: remove the check
937
        if (!$this->has('sonata.admin.admin_exporter')) {
938
            @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...
939
                'Not registering the exporter bundle is deprecated since version 3.14.'
940
                .' You must register it to be able to use the export action in 4.0.',
941
                E_USER_DEPRECATED
942
            );
943
            $allowedExportFormats = (array) $this->admin->getExportFormats();
944
945
            $class = (string) $this->admin->getClass();
946
            $filename = sprintf(
947
                'export_%s_%s.%s',
948
                strtolower((string) substr($class, strripos($class, '\\') + 1)),
949
                date('Y_m_d_H_i_s', strtotime('now')),
950
                $format
951
            );
952
            $exporter = $this->get('sonata.admin.exporter');
953
        } else {
954
            $adminExporter = $this->get('sonata.admin.admin_exporter');
955
            $allowedExportFormats = $adminExporter->getAvailableFormats($this->admin);
956
            $filename = $adminExporter->getExportFilename($this->admin, $format);
957
            $exporter = $this->get('sonata.exporter.exporter');
958
        }
959
960
        if (!\in_array($format, $allowedExportFormats, true)) {
961
            throw new \RuntimeException(
962
                sprintf(
963
                    'Export in format `%s` is not allowed for class: `%s`. Allowed formats are: `%s`',
964
                    $format,
965
                    $this->admin->getClass(),
966
                    implode(', ', $allowedExportFormats)
967
                )
968
            );
969
        }
970
971
        return $exporter->getResponse(
972
            $format,
973
            $filename,
974
            $this->admin->getDataSourceIterator()
975
        );
976
    }
977
978
    /**
979
     * Returns the Response object associated to the acl action.
980
     *
981
     * @param int|string|null $id
982
     *
983
     * @throws AccessDeniedException If access is not granted
984
     * @throws NotFoundHttpException If the object does not exist or the ACL is not enabled
985
     *
986
     * @return Response|RedirectResponse
987
     */
988
    public function aclAction($id = null)
0 ignored issues
show
Unused Code introduced by
The parameter $id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
989
    {
990
        $request = $this->getRequest();
991
992
        if (!$this->admin->isAclEnabled()) {
993
            throw $this->createNotFoundException('ACL are not enabled for this admin');
994
        }
995
996
        $id = $request->get($this->admin->getIdParameter());
997
998
        $object = $this->admin->getObject($id);
999
1000
        if (!$object) {
1001
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
1002
        }
1003
1004
        $this->admin->checkAccess('acl', $object);
1005
1006
        $this->admin->setSubject($object);
1007
        $aclUsers = $this->getAclUsers();
1008
        $aclRoles = $this->getAclRoles();
1009
1010
        $adminObjectAclManipulator = $this->get('sonata.admin.object.manipulator.acl.admin');
1011
        $adminObjectAclData = new AdminObjectAclData(
1012
            $this->admin,
1013
            $object,
1014
            $aclUsers,
1015
            $adminObjectAclManipulator->getMaskBuilderClass(),
1016
            $aclRoles
1017
        );
1018
1019
        $aclUsersForm = $adminObjectAclManipulator->createAclUsersForm($adminObjectAclData);
1020
        $aclRolesForm = $adminObjectAclManipulator->createAclRolesForm($adminObjectAclData);
1021
1022
        if ('POST' === $request->getMethod()) {
1023
            if ($request->request->has(AdminObjectAclManipulator::ACL_USERS_FORM_NAME)) {
1024
                $form = $aclUsersForm;
1025
                $updateMethod = 'updateAclUsers';
1026
            } elseif ($request->request->has(AdminObjectAclManipulator::ACL_ROLES_FORM_NAME)) {
1027
                $form = $aclRolesForm;
1028
                $updateMethod = 'updateAclRoles';
1029
            }
1030
1031
            if (isset($form)) {
1032
                $form->handleRequest($request);
1033
1034
                if ($form->isValid()) {
1035
                    $adminObjectAclManipulator->$updateMethod($adminObjectAclData);
0 ignored issues
show
Bug introduced by
The variable $updateMethod does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1036
                    $this->addFlash(
1037
                        'sonata_flash_success',
1038
                        $this->trans('flash_acl_edit_success', [], 'SonataAdminBundle')
1039
                    );
1040
1041
                    return new RedirectResponse($this->admin->generateObjectUrl('acl', $object));
1042
                }
1043
            }
1044
        }
1045
1046
        // NEXT_MAJOR: Remove this line and use commented line below it instead
1047
        $template = $this->admin->getTemplate('acl');
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...
1048
        // $template = $this->templateRegistry->getTemplate('acl');
1049
1050
        return $this->renderWithExtraParams($template, [
1051
            'action' => 'acl',
1052
            'permissions' => $adminObjectAclData->getUserPermissions(),
1053
            'object' => $object,
1054
            'users' => $aclUsers,
1055
            'roles' => $aclRoles,
1056
            'aclUsersForm' => $aclUsersForm->createView(),
1057
            'aclRolesForm' => $aclRolesForm->createView(),
1058
        ], null);
1059
    }
1060
1061
    /**
1062
     * @return Request
1063
     */
1064
    public function getRequest()
1065
    {
1066
        return $this->container->get('request_stack')->getCurrentRequest();
1067
    }
1068
1069
    /**
1070
     * Gets a container configuration parameter by its name.
1071
     *
1072
     * @param string $name The parameter name
1073
     *
1074
     * @return mixed
1075
     */
1076
    protected function getParameter($name)
1077
    {
1078
        return $this->container->getParameter($name);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Psr\Container\ContainerInterface as the method getParameter() does only exist in the following implementations of said interface: Container14\ProjectServiceContainer, ProjectServiceContainer, Symfony\Component\Depend...urationContainerBuilder, Symfony\Component\DependencyInjection\Container, Symfony\Component\Depend...ection\ContainerBuilder, Symfony\Component\Depend...\NoConstructorContainer, Symfony\Component\Depend...tainers\CustomContainer, Symfony\Component\Depend...ProjectServiceContainer, Symfony\Component\Depend...ProjectServiceContainer, Symfony_DI_PhpDumper_Test_Almost_Circular_Private, Symfony_DI_PhpDumper_Test_Almost_Circular_Public, Symfony_DI_PhpDumper_Test_Base64Parameters, Symfony_DI_PhpDumper_Test_Deep_Graph, Symfony_DI_PhpDumper_Test_EnvParameters, Symfony_DI_PhpDumper_Test_Inline_Self_Ref, Symfony_DI_PhpDumper_Test_Legacy_Privates, Symfony_DI_PhpDumper_Test_Rot13Parameters, Symfony_DI_PhpDumper_Test_Uninitialized_Reference.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1079
    }
1080
1081
    /**
1082
     * Render JSON.
1083
     *
1084
     * @param mixed $data
1085
     * @param int   $status
1086
     * @param array $headers
1087
     *
1088
     * @return Response with json encoded data
1089
     */
1090
    protected function renderJson($data, $status = 200, $headers = [])
1091
    {
1092
        return new JsonResponse($data, $status, $headers);
1093
    }
1094
1095
    /**
1096
     * Returns true if the request is a XMLHttpRequest.
1097
     *
1098
     * @return bool True if the request is an XMLHttpRequest, false otherwise
1099
     */
1100
    protected function isXmlHttpRequest()
1101
    {
1102
        $request = $this->getRequest();
1103
1104
        return $request->isXmlHttpRequest() || $request->get('_xml_http_request');
1105
    }
1106
1107
    /**
1108
     * Returns the correct RESTful verb, given either by the request itself or
1109
     * via the "_method" parameter.
1110
     *
1111
     * @return string HTTP method, either
1112
     */
1113
    protected function getRestMethod()
1114
    {
1115
        $request = $this->getRequest();
1116
1117
        if (Request::getHttpMethodParameterOverride() || !$request->request->has('_method')) {
1118
            return $request->getMethod();
1119
        }
1120
1121
        return $request->request->get('_method');
1122
    }
1123
1124
    /**
1125
     * Contextualize the admin class depends on the current request.
1126
     *
1127
     * @throws \RuntimeException
1128
     */
1129
    protected function configure(): void
1130
    {
1131
        $request = $this->getRequest();
1132
1133
        $adminCode = $request->get('_sonata_admin');
1134
1135
        if (!$adminCode) {
1136
            throw new \RuntimeException(sprintf(
1137
                'There is no `_sonata_admin` defined for the controller `%s` and the current route `%s`',
1138
                \get_class($this),
1139
                $request->get('_route')
1140
            ));
1141
        }
1142
1143
        $this->admin = $this->container->get('sonata.admin.pool')->getAdminByAdminCode($adminCode);
1144
1145
        if (!$this->admin) {
1146
            throw new \RuntimeException(sprintf(
1147
                'Unable to find the admin class related to the current controller (%s)',
1148
                \get_class($this)
1149
            ));
1150
        }
1151
1152
        $this->templateRegistry = $this->container->get($this->admin->getCode().'.template_registry');
1153
        if (!$this->templateRegistry instanceof TemplateRegistryInterface) {
1154
            throw new \RuntimeException(sprintf(
1155
                'Unable to find the template registry related to the current admin (%s)',
1156
                $this->admin->getCode()
1157
            ));
1158
        }
1159
1160
        $rootAdmin = $this->admin;
1161
1162
        while ($rootAdmin->isChild()) {
1163
            $rootAdmin->setCurrentChild(true);
1164
            $rootAdmin = $rootAdmin->getParent();
1165
        }
1166
1167
        $rootAdmin->setRequest($request);
1168
1169
        if ($request->get('uniqid')) {
1170
            $this->admin->setUniqid($request->get('uniqid'));
1171
        }
1172
    }
1173
1174
    /**
1175
     * Proxy for the logger service of the container.
1176
     * If no such service is found, a NullLogger is returned.
1177
     *
1178
     * @return LoggerInterface
1179
     */
1180
    protected function getLogger()
1181
    {
1182
        if ($this->container->has('logger')) {
1183
            $logger = $this->container->get('logger');
1184
            \assert($logger instanceof LoggerInterface);
1185
1186
            return $logger;
1187
        }
1188
1189
        return new NullLogger();
1190
    }
1191
1192
    /**
1193
     * Returns the base template name.
1194
     *
1195
     * @return string The template name
1196
     */
1197
    protected function getBaseTemplate()
1198
    {
1199
        if ($this->isXmlHttpRequest()) {
1200
            // NEXT_MAJOR: Remove this line and use commented line below it instead
1201
            return $this->admin->getTemplate('ajax');
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...
1202
            // return $this->templateRegistry->getTemplate('ajax');
1203
        }
1204
1205
        // NEXT_MAJOR: Remove this line and use commented line below it instead
1206
        return $this->admin->getTemplate('layout');
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...
1207
        // return $this->templateRegistry->getTemplate('layout');
1208
    }
1209
1210
    /**
1211
     * @throws \Exception
1212
     */
1213
    protected function handleModelManagerException(\Exception $e): void
1214
    {
1215
        if ($this->get('kernel')->isDebug()) {
1216
            throw $e;
1217
        }
1218
1219
        $context = ['exception' => $e];
1220
        if ($e->getPrevious()) {
1221
            $context['previous_exception_message'] = $e->getPrevious()->getMessage();
1222
        }
1223
        $this->getLogger()->error($e->getMessage(), $context);
1224
    }
1225
1226
    /**
1227
     * Redirect the user depend on this choice.
1228
     *
1229
     * @param object $object
1230
     *
1231
     * @return RedirectResponse
1232
     */
1233
    protected function redirectTo($object)
1234
    {
1235
        $request = $this->getRequest();
1236
1237
        $url = false;
1238
1239
        if (null !== $request->get('btn_update_and_list')) {
1240
            return $this->redirectToList();
1241
        }
1242
        if (null !== $request->get('btn_create_and_list')) {
1243
            return $this->redirectToList();
1244
        }
1245
1246
        if (null !== $request->get('btn_create_and_create')) {
1247
            $params = [];
1248
            if ($this->admin->hasActiveSubClass()) {
1249
                $params['subclass'] = $request->get('subclass');
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $params['subclass'] is correct as $request->get('subclass') (which targets Symfony\Component\HttpFoundation\Request::get()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1250
            }
1251
            $url = $this->admin->generateUrl('create', $params);
1252
        }
1253
1254
        if ('DELETE' === $this->getRestMethod()) {
1255
            return $this->redirectToList();
1256
        }
1257
1258
        if (!$url) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $url of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1259
            foreach (['edit', 'show'] as $route) {
1260
                if ($this->admin->hasRoute($route) && $this->admin->hasAccess($route, $object)) {
1261
                    $url = $this->admin->generateObjectUrl(
1262
                        $route,
1263
                        $object,
1264
                        $this->getSelectedTab($request)
1265
                    );
1266
1267
                    break;
1268
                }
1269
            }
1270
        }
1271
1272
        if (!$url) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $url of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1273
            return $this->redirectToList();
1274
        }
1275
1276
        return new RedirectResponse($url);
1277
    }
1278
1279
    /**
1280
     * Redirects the user to the list view.
1281
     *
1282
     * @return RedirectResponse
1283
     */
1284
    final protected function redirectToList()
1285
    {
1286
        $parameters = [];
1287
1288
        if ($filter = $this->admin->getFilterParameters()) {
1289
            $parameters['filter'] = $filter;
1290
        }
1291
1292
        return $this->redirect($this->admin->generateUrl('list', $parameters));
1293
    }
1294
1295
    /**
1296
     * Returns true if the preview is requested to be shown.
1297
     *
1298
     * @return bool
1299
     */
1300
    protected function isPreviewRequested()
1301
    {
1302
        $request = $this->getRequest();
1303
1304
        return null !== $request->get('btn_preview');
1305
    }
1306
1307
    /**
1308
     * Returns true if the preview has been approved.
1309
     *
1310
     * @return bool
1311
     */
1312
    protected function isPreviewApproved()
1313
    {
1314
        $request = $this->getRequest();
1315
1316
        return null !== $request->get('btn_preview_approve');
1317
    }
1318
1319
    /**
1320
     * Returns true if the request is in the preview workflow.
1321
     *
1322
     * That means either a preview is requested or the preview has already been shown
1323
     * and it got approved/declined.
1324
     *
1325
     * @return bool
1326
     */
1327
    protected function isInPreviewMode()
1328
    {
1329
        return $this->admin->supportsPreviewMode()
1330
        && ($this->isPreviewRequested()
1331
            || $this->isPreviewApproved()
1332
            || $this->isPreviewDeclined());
1333
    }
1334
1335
    /**
1336
     * Returns true if the preview has been declined.
1337
     *
1338
     * @return bool
1339
     */
1340
    protected function isPreviewDeclined()
1341
    {
1342
        $request = $this->getRequest();
1343
1344
        return null !== $request->get('btn_preview_decline');
1345
    }
1346
1347
    /**
1348
     * Gets ACL users.
1349
     *
1350
     * @return \Traversable
1351
     */
1352
    protected function getAclUsers()
1353
    {
1354
        $aclUsers = [];
1355
1356
        $userManagerServiceName = $this->container->getParameter('sonata.admin.security.acl_user_manager');
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Psr\Container\ContainerInterface as the method getParameter() does only exist in the following implementations of said interface: Container14\ProjectServiceContainer, ProjectServiceContainer, Symfony\Component\Depend...urationContainerBuilder, Symfony\Component\DependencyInjection\Container, Symfony\Component\Depend...ection\ContainerBuilder, Symfony\Component\Depend...\NoConstructorContainer, Symfony\Component\Depend...tainers\CustomContainer, Symfony\Component\Depend...ProjectServiceContainer, Symfony\Component\Depend...ProjectServiceContainer, Symfony_DI_PhpDumper_Test_Almost_Circular_Private, Symfony_DI_PhpDumper_Test_Almost_Circular_Public, Symfony_DI_PhpDumper_Test_Base64Parameters, Symfony_DI_PhpDumper_Test_Deep_Graph, Symfony_DI_PhpDumper_Test_EnvParameters, Symfony_DI_PhpDumper_Test_Inline_Self_Ref, Symfony_DI_PhpDumper_Test_Legacy_Privates, Symfony_DI_PhpDumper_Test_Rot13Parameters, Symfony_DI_PhpDumper_Test_Uninitialized_Reference.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1357
        if (null !== $userManagerServiceName && $this->has($userManagerServiceName)) {
1358
            $userManager = $this->get($userManagerServiceName);
1359
1360
            if (method_exists($userManager, 'findUsers')) {
1361
                $aclUsers = $userManager->findUsers();
1362
            }
1363
        }
1364
1365
        return \is_array($aclUsers) ? new \ArrayIterator($aclUsers) : $aclUsers;
1366
    }
1367
1368
    /**
1369
     * Gets ACL roles.
1370
     *
1371
     * @return \Traversable
1372
     */
1373
    protected function getAclRoles()
1374
    {
1375
        $aclRoles = [];
1376
        $roleHierarchy = $this->container->getParameter('security.role_hierarchy.roles');
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Psr\Container\ContainerInterface as the method getParameter() does only exist in the following implementations of said interface: Container14\ProjectServiceContainer, ProjectServiceContainer, Symfony\Component\Depend...urationContainerBuilder, Symfony\Component\DependencyInjection\Container, Symfony\Component\Depend...ection\ContainerBuilder, Symfony\Component\Depend...\NoConstructorContainer, Symfony\Component\Depend...tainers\CustomContainer, Symfony\Component\Depend...ProjectServiceContainer, Symfony\Component\Depend...ProjectServiceContainer, Symfony_DI_PhpDumper_Test_Almost_Circular_Private, Symfony_DI_PhpDumper_Test_Almost_Circular_Public, Symfony_DI_PhpDumper_Test_Base64Parameters, Symfony_DI_PhpDumper_Test_Deep_Graph, Symfony_DI_PhpDumper_Test_EnvParameters, Symfony_DI_PhpDumper_Test_Inline_Self_Ref, Symfony_DI_PhpDumper_Test_Legacy_Privates, Symfony_DI_PhpDumper_Test_Rot13Parameters, Symfony_DI_PhpDumper_Test_Uninitialized_Reference.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1377
        $pool = $this->container->get('sonata.admin.pool');
1378
1379
        foreach ($pool->getAdminServiceIds() as $id) {
1380
            try {
1381
                $admin = $pool->getInstance($id);
1382
            } catch (\Exception $e) {
1383
                continue;
1384
            }
1385
1386
            $baseRole = $admin->getSecurityHandler()->getBaseRole($admin);
1387
            foreach ($admin->getSecurityInformation() as $role => $permissions) {
1388
                $role = sprintf($baseRole, $role);
1389
                $aclRoles[] = $role;
1390
            }
1391
        }
1392
1393
        foreach ($roleHierarchy as $name => $roles) {
1394
            $aclRoles[] = $name;
1395
            $aclRoles = array_merge($aclRoles, $roles);
1396
        }
1397
1398
        $aclRoles = array_unique($aclRoles);
1399
1400
        return \is_array($aclRoles) ? new \ArrayIterator($aclRoles) : $aclRoles;
1401
    }
1402
1403
    /**
1404
     * Validate CSRF token for action without form.
1405
     *
1406
     * @param string $intention
1407
     *
1408
     * @throws HttpException
1409
     */
1410
    protected function validateCsrfToken($intention): void
1411
    {
1412
        $request = $this->getRequest();
1413
        $token = $request->request->get('_sonata_csrf_token', false);
1414
1415
        if ($this->container->has('security.csrf.token_manager')) {
1416
            $valid = $this->container->get('security.csrf.token_manager')->isTokenValid(new CsrfToken($intention, $token));
1417
        } else {
1418
            return;
1419
        }
1420
1421
        if (!$valid) {
1422
            throw new HttpException(400, 'The csrf token is not valid, CSRF attack?');
1423
        }
1424
    }
1425
1426
    /**
1427
     * Escape string for html output.
1428
     *
1429
     * @param string $s
1430
     *
1431
     * @return string
1432
     */
1433
    protected function escapeHtml($s)
1434
    {
1435
        return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
1436
    }
1437
1438
    /**
1439
     * Get CSRF token.
1440
     *
1441
     * @param string $intention
1442
     *
1443
     * @return string|false
1444
     */
1445
    protected function getCsrfToken($intention)
1446
    {
1447
        if ($this->container->has('security.csrf.token_manager')) {
1448
            return $this->container->get('security.csrf.token_manager')->getToken($intention)->getValue();
1449
        }
1450
1451
        return false;
1452
    }
1453
1454
    /**
1455
     * This method can be overloaded in your custom CRUD controller.
1456
     * It's called from createAction.
1457
     *
1458
     * @param mixed $object
1459
     *
1460
     * @return Response|null
1461
     */
1462
    protected function preCreate(Request $request, $object)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1463
    {
1464
    }
1465
1466
    /**
1467
     * This method can be overloaded in your custom CRUD controller.
1468
     * It's called from editAction.
1469
     *
1470
     * @param mixed $object
1471
     *
1472
     * @return Response|null
1473
     */
1474
    protected function preEdit(Request $request, $object)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1475
    {
1476
    }
1477
1478
    /**
1479
     * This method can be overloaded in your custom CRUD controller.
1480
     * It's called from deleteAction.
1481
     *
1482
     * @param mixed $object
1483
     *
1484
     * @return Response|null
1485
     */
1486
    protected function preDelete(Request $request, $object)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1487
    {
1488
    }
1489
1490
    /**
1491
     * This method can be overloaded in your custom CRUD controller.
1492
     * It's called from showAction.
1493
     *
1494
     * @param mixed $object
1495
     *
1496
     * @return Response|null
1497
     */
1498
    protected function preShow(Request $request, $object)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1499
    {
1500
    }
1501
1502
    /**
1503
     * This method can be overloaded in your custom CRUD controller.
1504
     * It's called from listAction.
1505
     *
1506
     * @return Response|null
1507
     */
1508
    protected function preList(Request $request)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1509
    {
1510
    }
1511
1512
    /**
1513
     * Translate a message id.
1514
     *
1515
     * @param string $id
1516
     * @param string $domain
1517
     * @param string $locale
1518
     *
1519
     * @return string translated string
1520
     */
1521
    final protected function trans($id, array $parameters = [], $domain = null, $locale = null)
1522
    {
1523
        $domain = $domain ?: $this->admin->getTranslationDomain();
1524
1525
        return $this->get('translator')->trans($id, $parameters, $domain, $locale);
1526
    }
1527
1528
    private function getSelectedTab(Request $request)
1529
    {
1530
        return array_filter(['_tab' => $request->request->get('_tab')]);
1531
    }
1532
1533
    private function checkParentChildAssociation(Request $request, $object): void
1534
    {
1535
        if (!($parentAdmin = $this->admin->getParent())) {
1536
            return;
1537
        }
1538
1539
        // NEXT_MAJOR: remove this check
1540
        if (!$this->admin->getParentAssociationMapping()) {
0 ignored issues
show
Bug introduced by
The method getParentAssociationMapping() does not exist on Sonata\AdminBundle\Admin\AdminInterface. Did you maybe mean getParent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1541
            return;
1542
        }
1543
1544
        $parentId = $request->get($parentAdmin->getIdParameter());
1545
1546
        $propertyAccessor = PropertyAccess::createPropertyAccessor();
1547
        $propertyPath = new PropertyPath($this->admin->getParentAssociationMapping());
0 ignored issues
show
Bug introduced by
The method getParentAssociationMapping() does not exist on Sonata\AdminBundle\Admin\AdminInterface. Did you maybe mean getParent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1548
1549
        if ($parentAdmin->getObject($parentId) !== $propertyAccessor->getValue($object, $propertyPath)) {
1550
            // NEXT_MAJOR: make this exception
1551
            @trigger_error("Accessing a child that isn't connected to a given parent is deprecated since 3.34"
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...
1552
                ." and won't be allowed in 4.0.",
1553
                E_USER_DEPRECATED
1554
            );
1555
        }
1556
    }
1557
1558
    /**
1559
     * Sets the admin form theme to form view. Used for compatibility between Symfony versions.
1560
     */
1561
    private function setFormTheme(FormView $formView, array $theme = null): void
1562
    {
1563
        $twig = $this->get('twig');
1564
1565
        // BC for Symfony < 3.2 where this runtime does not exists
1566
        if (!method_exists(AppVariable::class, 'getToken')) {
1567
            $twig->getExtension(FormExtension::class)->renderer->setTheme($formView, $theme);
1568
1569
            return;
1570
        }
1571
1572
        // BC for Symfony < 3.4 where runtime should be TwigRenderer
1573
        if (!method_exists(DebugCommand::class, 'getLoaderPaths')) {
1574
            $twig->getRuntime(TwigRenderer::class)->setTheme($formView, $theme);
1575
1576
            return;
1577
        }
1578
1579
        $twig->getRuntime(FormRenderer::class)->setTheme($formView, $theme);
1580
    }
1581
}
1582