Completed
Pull Request — master (#5229)
by Grégoire
30:41
created

CRUDController::renderWithExtraParams()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 16
rs 9.7333
c 0
b 0
f 0
cc 2
nc 2
nop 3
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 58 and the first side effect is on line 52.

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\Form;
36
use Symfony\Component\Form\FormRenderer;
37
use Symfony\Component\Form\FormView;
38
use Symfony\Component\HttpFoundation\JsonResponse;
39
use Symfony\Component\HttpFoundation\RedirectResponse;
40
use Symfony\Component\HttpFoundation\Request;
41
use Symfony\Component\HttpFoundation\Response;
42
use Symfony\Component\HttpKernel\Exception\HttpException;
43
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
44
use Symfony\Component\PropertyAccess\PropertyAccess;
45
use Symfony\Component\PropertyAccess\PropertyPath;
46
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
47
use Symfony\Component\Security\Csrf\CsrfToken;
48
49
// BC for Symfony < 3.3 where this trait does not exist
50
// NEXT_MAJOR: Remove the polyfill and inherit from \Symfony\Bundle\FrameworkBundle\Controller\Controller again
51
if (!trait_exists(ControllerTrait::class)) {
52
    require_once __DIR__.'/PolyfillControllerTrait.php';
53
}
54
55
/**
56
 * @author Thomas Rabaix <[email protected]>
57
 */
58
class CRUDController implements ContainerAwareInterface
59
{
60
    // NEXT_MAJOR: Don't use these traits anymore (inherit from Controller instead)
61
    use ControllerTrait, ContainerAwareTrait {
62
        ControllerTrait::render as originalRender;
63
    }
64
65
    /**
66
     * The related Admin class.
67
     *
68
     * @var AdminInterface
69
     */
70
    protected $admin;
71
72
    /**
73
     * The template registry of the related Admin class.
74
     *
75
     * @var TemplateRegistryInterface
76
     */
77
    private $templateRegistry;
78
79
    // BC for Symfony 3.3 where ControllerTrait exists but does not contain get() and has() methods.
80
    public function __call($method, $arguments)
81
    {
82
        if (\in_array($method, ['get', 'has'])) {
83
            return \call_user_func_array([$this->container, $method], $arguments);
84
        }
85
86
        if (method_exists($this, 'proxyToControllerClass')) {
87
            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...
88
        }
89
90
        throw new \LogicException('Call to undefined method '.__CLASS__.'::'.$method);
91
    }
92
93
    public function setContainer(ContainerInterface $container = null): void
94
    {
95
        $this->container = $container;
96
97
        $this->configure();
98
    }
99
100
    /**
101
     * NEXT_MAJOR: Remove this method.
102
     *
103
     * @see renderWithExtraParams()
104
     *
105
     * @param string $view       The view name
106
     * @param array  $parameters An array of parameters to pass to the view
107
     *
108
     * @return Response A Response instance
109
     *
110
     * @deprecated since version 3.27, to be removed in 4.0. Use Sonata\AdminBundle\Controller\CRUDController::renderWithExtraParams() instead.
111
     */
112
    public function render($view, array $parameters = [], Response $response = null)
113
    {
114
        @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...
115
            'Method '.__CLASS__.'::render has been renamed to '.__CLASS__.'::renderWithExtraParams.',
116
            E_USER_DEPRECATED
117
        );
118
119
        return $this->renderWithExtraParams($view, $parameters, $response);
120
    }
121
122
    /**
123
     * Renders a view while passing mandatory parameters on to the template.
124
     *
125
     * @param string $view The view name
126
     *
127
     * @return Response A Response instance
128
     */
129
    public function renderWithExtraParams($view, array $parameters = [], Response $response = null)
130
    {
131
        if (!$this->isXmlHttpRequest()) {
132
            $parameters['breadcrumbs_builder'] = $this->get('sonata.admin.breadcrumbs_builder');
133
        }
134
        $parameters['admin'] = $parameters['admin'] ??
135
            $this->admin;
136
137
        $parameters['base_template'] = $parameters['base_template'] ??
138
            $this->getBaseTemplate();
139
140
        $parameters['admin_pool'] = $this->get('sonata.admin.pool');
141
142
        //NEXT_MAJOR: Remove method alias and use $this->render() directly.
143
        return $this->originalRender($view, $parameters, $response);
144
    }
145
146
    /**
147
     * List action.
148
     *
149
     * @throws AccessDeniedException If access is not granted
150
     *
151
     * @return Response
152
     */
153
    public function listAction()
154
    {
155
        $request = $this->getRequest();
156
157
        $this->admin->checkAccess('list');
158
159
        $preResponse = $this->preList($request);
160
        if (null !== $preResponse) {
161
            return $preResponse;
162
        }
163
164
        if ($listMode = $request->get('_list_mode')) {
165
            $this->admin->setListMode($listMode);
166
        }
167
168
        $datagrid = $this->admin->getDatagrid();
169
        $formView = $datagrid->getForm()->createView();
170
171
        // set the theme for the current Admin Form
172
        $this->setFormTheme($formView, $this->admin->getFilterTheme());
173
174
        // NEXT_MAJOR: Remove this line and use commented line below it instead
175
        $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...
176
        // $template = $this->templateRegistry->getTemplate('list');
177
178
        return $this->renderWithExtraParams($template, [
179
            'action' => 'list',
180
            'form' => $formView,
181
            'datagrid' => $datagrid,
182
            'csrf_token' => $this->getCsrfToken('sonata.batch'),
183
            'export_formats' => $this->has('sonata.admin.admin_exporter') ?
184
                $this->get('sonata.admin.admin_exporter')->getAvailableFormats($this->admin) :
185
                $this->admin->getExportFormats(),
186
        ], null);
187
    }
188
189
    /**
190
     * Execute a batch delete.
191
     *
192
     * @throws AccessDeniedException If access is not granted
193
     *
194
     * @return RedirectResponse
195
     */
196
    public function batchActionDelete(ProxyQueryInterface $query)
197
    {
198
        $this->admin->checkAccess('batchDelete');
199
200
        $modelManager = $this->admin->getModelManager();
201
202
        try {
203
            $modelManager->batchDelete($this->admin->getClass(), $query);
204
            $this->addFlash(
205
                'sonata_flash_success',
206
                $this->trans('flash_batch_delete_success', [], 'SonataAdminBundle')
207
            );
208
        } catch (ModelManagerException $e) {
209
            $this->handleModelManagerException($e);
210
            $this->addFlash(
211
                'sonata_flash_error',
212
                $this->trans('flash_batch_delete_error', [], 'SonataAdminBundle')
213
            );
214
        }
215
216
        return $this->redirectToList();
217
    }
218
219
    /**
220
     * Delete action.
221
     *
222
     * @param int|string|null $id
223
     *
224
     * @throws NotFoundHttpException If the object does not exist
225
     * @throws AccessDeniedException If access is not granted
226
     *
227
     * @return Response|RedirectResponse
228
     */
229
    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...
230
    {
231
        $request = $this->getRequest();
232
        $id = $request->get($this->admin->getIdParameter());
233
        $object = $this->admin->getObject($id);
234
235
        if (!$object) {
236
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
237
        }
238
239
        $this->checkParentChildAssociation($request, $object);
240
241
        $this->admin->checkAccess('delete', $object);
242
243
        $preResponse = $this->preDelete($request, $object);
244
        if (null !== $preResponse) {
245
            return $preResponse;
246
        }
247
248
        if ('DELETE' == $this->getRestMethod()) {
249
            // check the csrf token
250
            $this->validateCsrfToken('sonata.delete');
251
252
            $objectName = $this->admin->toString($object);
253
254
            try {
255
                $this->admin->delete($object);
256
257
                if ($this->isXmlHttpRequest()) {
258
                    return $this->renderJson(['result' => 'ok'], 200, []);
259
                }
260
261
                $this->addFlash(
262
                    'sonata_flash_success',
263
                    $this->trans(
264
                        'flash_delete_success',
265
                        ['%name%' => $this->escapeHtml($objectName)],
266
                        'SonataAdminBundle'
267
                    )
268
                );
269
            } catch (ModelManagerException $e) {
270
                $this->handleModelManagerException($e);
271
272
                if ($this->isXmlHttpRequest()) {
273
                    return $this->renderJson(['result' => 'error'], 200, []);
274
                }
275
276
                $this->addFlash(
277
                    'sonata_flash_error',
278
                    $this->trans(
279
                        'flash_delete_error',
280
                        ['%name%' => $this->escapeHtml($objectName)],
281
                        'SonataAdminBundle'
282
                    )
283
                );
284
            }
285
286
            return $this->redirectTo($object);
287
        }
288
289
        // NEXT_MAJOR: Remove this line and use commented line below it instead
290
        $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...
291
        // $template = $this->templateRegistry->getTemplate('delete');
292
293
        return $this->renderWithExtraParams($template, [
294
            'object' => $object,
295
            'action' => 'delete',
296
            'csrf_token' => $this->getCsrfToken('sonata.delete'),
297
        ], null);
298
    }
299
300
    /**
301
     * Edit action.
302
     *
303
     * @param int|string|null $id
304
     *
305
     * @throws NotFoundHttpException If the object does not exist
306
     * @throws \RuntimeException     If no editable field is defined
307
     * @throws AccessDeniedException If access is not granted
308
     *
309
     * @return Response|RedirectResponse
310
     */
311
    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...
312
    {
313
        $request = $this->getRequest();
314
        // the key used to lookup the template
315
        $templateKey = 'edit';
316
317
        $id = $request->get($this->admin->getIdParameter());
318
        $existingObject = $this->admin->getObject($id);
319
320
        if (!$existingObject) {
321
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
322
        }
323
324
        $this->checkParentChildAssociation($request, $existingObject);
325
326
        $this->admin->checkAccess('edit', $existingObject);
327
328
        $preResponse = $this->preEdit($request, $existingObject);
329
        if (null !== $preResponse) {
330
            return $preResponse;
331
        }
332
333
        $this->admin->setSubject($existingObject);
334
        $objectId = $this->admin->getNormalizedIdentifier($existingObject);
335
336
        $form = $this->admin->getForm();
337
        \assert($form instanceof Form);
338
339
        if (!\is_array($fields = $form->all()) || 0 === \count($fields)) {
340
            throw new \RuntimeException(
341
                'No editable field defined. Did you forget to implement the "configureFormFields" method?'
342
            );
343
        }
344
345
        $form->setData($existingObject);
346
        $form->handleRequest($request);
347
348
        if ($form->isSubmitted()) {
349
            $isFormValid = $form->isValid();
350
351
            // persist if the form was valid and if in preview mode the preview was approved
352
            if ($isFormValid && (!$this->isInPreviewMode() || $this->isPreviewApproved())) {
353
                $submittedObject = $form->getData();
354
                $this->admin->setSubject($submittedObject);
355
356
                try {
357
                    $existingObject = $this->admin->update($submittedObject);
358
359
                    if ($this->isXmlHttpRequest()) {
360
                        return $this->renderJson([
361
                            'result' => 'ok',
362
                            'objectId' => $objectId,
363
                            'objectName' => $this->escapeHtml($this->admin->toString($existingObject)),
364
                        ], 200, []);
365
                    }
366
367
                    $this->addFlash(
368
                        'sonata_flash_success',
369
                        $this->trans(
370
                            'flash_edit_success',
371
                            ['%name%' => $this->escapeHtml($this->admin->toString($existingObject))],
372
                            'SonataAdminBundle'
373
                        )
374
                    );
375
376
                    // redirect to edit mode
377
                    return $this->redirectTo($existingObject);
378
                } catch (ModelManagerException $e) {
379
                    $this->handleModelManagerException($e);
380
381
                    $isFormValid = false;
382
                } catch (LockException $e) {
383
                    $this->addFlash('sonata_flash_error', $this->trans('flash_lock_error', [
384
                        '%name%' => $this->escapeHtml($this->admin->toString($existingObject)),
385
                        '%link_start%' => '<a href="'.$this->admin->generateObjectUrl('edit', $existingObject).'">',
386
                        '%link_end%' => '</a>',
387
                    ], 'SonataAdminBundle'));
388
                }
389
            }
390
391
            // show an error message if the form failed validation
392
            if (!$isFormValid) {
393
                if (!$this->isXmlHttpRequest()) {
394
                    $this->addFlash(
395
                        'sonata_flash_error',
396
                        $this->trans(
397
                            'flash_edit_error',
398
                            ['%name%' => $this->escapeHtml($this->admin->toString($existingObject))],
399
                            'SonataAdminBundle'
400
                        )
401
                    );
402
                }
403
            } elseif ($this->isPreviewRequested()) {
404
                // enable the preview template if the form was valid and preview was requested
405
                $templateKey = 'preview';
406
                $this->admin->getShow();
407
            }
408
        }
409
410
        $formView = $form->createView();
411
        // set the theme for the current Admin Form
412
        $this->setFormTheme($formView, $this->admin->getFormTheme());
413
414
        // NEXT_MAJOR: Remove this line and use commented line below it instead
415
        $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...
416
        // $template = $this->templateRegistry->getTemplate($templateKey);
417
418
        return $this->renderWithExtraParams($template, [
419
            'action' => 'edit',
420
            'form' => $formView,
421
            'object' => $existingObject,
422
            'objectId' => $objectId,
423
        ], null);
424
    }
425
426
    /**
427
     * Batch action.
428
     *
429
     * @throws NotFoundHttpException If the HTTP method is not POST
430
     * @throws \RuntimeException     If the batch action is not defined
431
     *
432
     * @return Response|RedirectResponse
433
     */
434
    public function batchAction()
435
    {
436
        $request = $this->getRequest();
437
        $restMethod = $this->getRestMethod();
438
439
        if ('POST' !== $restMethod) {
440
            throw $this->createNotFoundException(sprintf('Invalid request type "%s", POST expected', $restMethod));
441
        }
442
443
        // check the csrf token
444
        $this->validateCsrfToken('sonata.batch');
445
446
        $confirmation = $request->get('confirmation', false);
447
448
        if ($data = json_decode($request->get('data', ''), true)) {
449
            $action = $data['action'];
450
            $idx = $data['idx'];
451
            $allElements = $data['all_elements'];
452
            $request->request->replace(array_merge($request->request->all(), $data));
453
        } else {
454
            $request->request->set('idx', $request->get('idx', []));
455
            $request->request->set('all_elements', $request->get('all_elements', false));
456
457
            $action = $request->get('action');
458
            $idx = $request->get('idx');
459
            $allElements = $request->get('all_elements');
460
            $data = $request->request->all();
461
462
            unset($data['_sonata_csrf_token']);
463
        }
464
465
        $batchActions = $this->admin->getBatchActions();
466
        if (!array_key_exists($action, $batchActions)) {
467
            throw new \RuntimeException(sprintf('The `%s` batch action is not defined', $action));
468
        }
469
470
        $camelizedAction = Inflector::classify($action);
471
        $isRelevantAction = sprintf('batchAction%sIsRelevant', ucfirst($camelizedAction));
472
473
        if (method_exists($this, $isRelevantAction)) {
474
            $nonRelevantMessage = \call_user_func([$this, $isRelevantAction], $idx, $allElements, $request);
475
        } else {
476
            $nonRelevantMessage = 0 != \count($idx) || $allElements; // at least one item is selected
477
        }
478
479
        if (!$nonRelevantMessage) { // default non relevant message (if false of null)
480
            $nonRelevantMessage = 'flash_batch_empty';
481
        }
482
483
        $datagrid = $this->admin->getDatagrid();
484
        $datagrid->buildPager();
485
486
        if (true !== $nonRelevantMessage) {
487
            $this->addFlash(
488
                'sonata_flash_info',
489
                $this->trans($nonRelevantMessage, [], 'SonataAdminBundle')
490
            );
491
492
            return $this->redirectToList();
493
        }
494
495
        $askConfirmation = $batchActions[$action]['ask_confirmation'] ??
496
            true;
497
498
        if ($askConfirmation && 'ok' != $confirmation) {
499
            $actionLabel = $batchActions[$action]['label'];
500
            $batchTranslationDomain = $batchActions[$action]['translation_domain'] ??
501
                $this->admin->getTranslationDomain();
502
503
            $formView = $datagrid->getForm()->createView();
504
            $this->setFormTheme($formView, $this->admin->getFilterTheme());
505
506
            // NEXT_MAJOR: Remove this line and use commented line below it instead
507
            $template = $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 = $this->templateRegistry->getTemplate('batch_confirmation');
509
510
            return $this->renderWithExtraParams($template, [
511
                'action' => 'list',
512
                'action_label' => $actionLabel,
513
                'batch_translation_domain' => $batchTranslationDomain,
514
                'datagrid' => $datagrid,
515
                'form' => $formView,
516
                'data' => $data,
517
                'csrf_token' => $this->getCsrfToken('sonata.batch'),
518
            ], null);
519
        }
520
521
        // execute the action, batchActionXxxxx
522
        $finalAction = sprintf('batchAction%s', $camelizedAction);
523
        if (!method_exists($this, $finalAction)) {
524
            throw new \RuntimeException(sprintf('A `%s::%s` method must be callable', \get_class($this), $finalAction));
525
        }
526
527
        $query = $datagrid->getQuery();
528
529
        $query->setFirstResult(null);
530
        $query->setMaxResults(null);
531
532
        $this->admin->preBatchAction($action, $query, $idx, $allElements);
533
534
        if (\count($idx) > 0) {
535
            $this->admin->getModelManager()->addIdentifiersToQuery($this->admin->getClass(), $query, $idx);
536
        } elseif (!$allElements) {
537
            $this->addFlash(
538
                'sonata_flash_info',
539
                $this->trans('flash_batch_no_elements_processed', [], 'SonataAdminBundle')
540
            );
541
542
            return $this->redirectToList();
543
        }
544
545
        return \call_user_func([$this, $finalAction], $query, $request);
546
    }
547
548
    /**
549
     * Create action.
550
     *
551
     * @throws AccessDeniedException If access is not granted
552
     * @throws \RuntimeException     If no editable field is defined
553
     *
554
     * @return Response
555
     */
556
    public function createAction()
557
    {
558
        $request = $this->getRequest();
559
        // the key used to lookup the template
560
        $templateKey = 'edit';
561
562
        $this->admin->checkAccess('create');
563
564
        $class = new \ReflectionClass($this->admin->hasActiveSubClass() ? $this->admin->getActiveSubClass() : $this->admin->getClass());
565
566
        if ($class->isAbstract()) {
567
            return $this->renderWithExtraParams(
568
                '@SonataAdmin/CRUD/select_subclass.html.twig',
569
                [
570
                    'base_template' => $this->getBaseTemplate(),
571
                    'admin' => $this->admin,
572
                    'action' => 'create',
573
                ],
574
                null
575
            );
576
        }
577
578
        $newObject = $this->admin->getNewInstance();
579
580
        $preResponse = $this->preCreate($request, $newObject);
581
        if (null !== $preResponse) {
582
            return $preResponse;
583
        }
584
585
        $this->admin->setSubject($newObject);
586
587
        $form = $this->admin->getForm();
588
        \assert($form instanceof Form);
589
590
        if (!\is_array($fields = $form->all()) || 0 === \count($fields)) {
591
            throw new \RuntimeException(
592
                'No editable field defined. Did you forget to implement the "configureFormFields" method?'
593
            );
594
        }
595
596
        $form->setData($newObject);
597
        $form->handleRequest($request);
598
599
        if ($form->isSubmitted()) {
600
            $isFormValid = $form->isValid();
601
602
            // persist if the form was valid and if in preview mode the preview was approved
603
            if ($isFormValid && (!$this->isInPreviewMode() || $this->isPreviewApproved())) {
604
                $submittedObject = $form->getData();
605
                $this->admin->setSubject($submittedObject);
606
                $this->admin->checkAccess('create', $submittedObject);
607
608
                try {
609
                    $newObject = $this->admin->create($submittedObject);
610
611
                    if ($this->isXmlHttpRequest()) {
612
                        return $this->renderJson([
613
                            'result' => 'ok',
614
                            'objectId' => $this->admin->getNormalizedIdentifier($newObject),
615
                        ], 200, []);
616
                    }
617
618
                    $this->addFlash(
619
                        'sonata_flash_success',
620
                        $this->trans(
621
                            'flash_create_success',
622
                            ['%name%' => $this->escapeHtml($this->admin->toString($newObject))],
623
                            'SonataAdminBundle'
624
                        )
625
                    );
626
627
                    // redirect to edit mode
628
                    return $this->redirectTo($newObject);
629
                } catch (ModelManagerException $e) {
630
                    $this->handleModelManagerException($e);
631
632
                    $isFormValid = false;
633
                }
634
            }
635
636
            // show an error message if the form failed validation
637
            if (!$isFormValid) {
638
                if (!$this->isXmlHttpRequest()) {
639
                    $this->addFlash(
640
                        'sonata_flash_error',
641
                        $this->trans(
642
                            'flash_create_error',
643
                            ['%name%' => $this->escapeHtml($this->admin->toString($newObject))],
644
                            'SonataAdminBundle'
645
                        )
646
                    );
647
                }
648
            } elseif ($this->isPreviewRequested()) {
649
                // pick the preview template if the form was valid and preview was requested
650
                $templateKey = 'preview';
651
                $this->admin->getShow();
652
            }
653
        }
654
655
        $formView = $form->createView();
656
        // set the theme for the current Admin Form
657
        $this->setFormTheme($formView, $this->admin->getFormTheme());
658
659
        // NEXT_MAJOR: Remove this line and use commented line below it instead
660
        $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...
661
        // $template = $this->templateRegistry->getTemplate($templateKey);
662
663
        return $this->renderWithExtraParams($template, [
664
            'action' => 'create',
665
            'form' => $formView,
666
            'object' => $newObject,
667
            'objectId' => null,
668
        ], null);
669
    }
670
671
    /**
672
     * Show action.
673
     *
674
     * @param int|string|null $id
675
     *
676
     * @throws NotFoundHttpException If the object does not exist
677
     * @throws AccessDeniedException If access is not granted
678
     *
679
     * @return Response
680
     */
681
    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...
682
    {
683
        $request = $this->getRequest();
684
        $id = $request->get($this->admin->getIdParameter());
685
686
        $object = $this->admin->getObject($id);
687
688
        if (!$object) {
689
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
690
        }
691
692
        $this->checkParentChildAssociation($request, $object);
693
694
        $this->admin->checkAccess('show', $object);
695
696
        $preResponse = $this->preShow($request, $object);
697
        if (null !== $preResponse) {
698
            return $preResponse;
699
        }
700
701
        $this->admin->setSubject($object);
702
703
        $fields = $this->admin->getShow();
704
        \assert($fields instanceof FieldDescriptionCollection);
705
706
        // NEXT_MAJOR: replace deprecation with exception
707
        if (!\is_array($fields->getElements()) || 0 === $fields->count()) {
708
            @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...
709
                'Calling this method without implementing "configureShowFields"'
710
                .' is not supported since 3.x'
711
                .' and will no longer be possible in 4.0',
712
                E_USER_DEPRECATED
713
            );
714
        }
715
716
        // NEXT_MAJOR: Remove this line and use commented line below it instead
717
        $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...
718
        //$template = $this->templateRegistry->getTemplate('show');
719
720
        return $this->renderWithExtraParams($template, [
721
            'action' => 'show',
722
            'object' => $object,
723
            'elements' => $fields,
724
        ], null);
725
    }
726
727
    /**
728
     * Show history revisions for object.
729
     *
730
     * @param int|string|null $id
731
     *
732
     * @throws AccessDeniedException If access is not granted
733
     * @throws NotFoundHttpException If the object does not exist or the audit reader is not available
734
     *
735
     * @return Response
736
     */
737
    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...
738
    {
739
        $request = $this->getRequest();
740
        $id = $request->get($this->admin->getIdParameter());
741
742
        $object = $this->admin->getObject($id);
743
744
        if (!$object) {
745
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
746
        }
747
748
        $this->admin->checkAccess('history', $object);
749
750
        $manager = $this->get('sonata.admin.audit.manager');
751
752
        if (!$manager->hasReader($this->admin->getClass())) {
753
            throw $this->createNotFoundException(
754
                sprintf(
755
                    'unable to find the audit reader for class : %s',
756
                    $this->admin->getClass()
757
                )
758
            );
759
        }
760
761
        $reader = $manager->getReader($this->admin->getClass());
762
763
        $revisions = $reader->findRevisions($this->admin->getClass(), $id);
764
765
        // NEXT_MAJOR: Remove this line and use commented line below it instead
766
        $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...
767
        // $template = $this->templateRegistry->getTemplate('history');
768
769
        return $this->renderWithExtraParams($template, [
770
            'action' => 'history',
771
            'object' => $object,
772
            'revisions' => $revisions,
773
            'currentRevision' => $revisions ? current($revisions) : false,
774
        ], null);
775
    }
776
777
    /**
778
     * View history revision of object.
779
     *
780
     * @param int|string|null $id
781
     * @param string|null     $revision
782
     *
783
     * @throws AccessDeniedException If access is not granted
784
     * @throws NotFoundHttpException If the object or revision does not exist or the audit reader is not available
785
     *
786
     * @return Response
787
     */
788
    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...
789
    {
790
        $request = $this->getRequest();
791
        $id = $request->get($this->admin->getIdParameter());
792
793
        $object = $this->admin->getObject($id);
794
795
        if (!$object) {
796
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
797
        }
798
799
        $this->admin->checkAccess('historyViewRevision', $object);
800
801
        $manager = $this->get('sonata.admin.audit.manager');
802
803
        if (!$manager->hasReader($this->admin->getClass())) {
804
            throw $this->createNotFoundException(
805
                sprintf(
806
                    'unable to find the audit reader for class : %s',
807
                    $this->admin->getClass()
808
                )
809
            );
810
        }
811
812
        $reader = $manager->getReader($this->admin->getClass());
813
814
        // retrieve the revisioned object
815
        $object = $reader->find($this->admin->getClass(), $id, $revision);
816
817
        if (!$object) {
818
            throw $this->createNotFoundException(
819
                sprintf(
820
                    'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
821
                    $id,
822
                    $revision,
823
                    $this->admin->getClass()
824
                )
825
            );
826
        }
827
828
        $this->admin->setSubject($object);
829
830
        // NEXT_MAJOR: Remove this line and use commented line below it instead
831
        $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...
832
        // $template = $this->templateRegistry->getTemplate('show');
833
834
        return $this->renderWithExtraParams($template, [
835
            'action' => 'show',
836
            'object' => $object,
837
            'elements' => $this->admin->getShow(),
838
        ], null);
839
    }
840
841
    /**
842
     * Compare history revisions of object.
843
     *
844
     * @param int|string|null $id
845
     * @param int|string|null $base_revision
846
     * @param int|string|null $compare_revision
847
     *
848
     * @throws AccessDeniedException If access is not granted
849
     * @throws NotFoundHttpException If the object or revision does not exist or the audit reader is not available
850
     *
851
     * @return Response
852
     */
853
    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...
854
    {
855
        $request = $this->getRequest();
856
857
        $this->admin->checkAccess('historyCompareRevisions');
858
859
        $id = $request->get($this->admin->getIdParameter());
860
861
        $object = $this->admin->getObject($id);
862
863
        if (!$object) {
864
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
865
        }
866
867
        $manager = $this->get('sonata.admin.audit.manager');
868
869
        if (!$manager->hasReader($this->admin->getClass())) {
870
            throw $this->createNotFoundException(
871
                sprintf(
872
                    'unable to find the audit reader for class : %s',
873
                    $this->admin->getClass()
874
                )
875
            );
876
        }
877
878
        $reader = $manager->getReader($this->admin->getClass());
879
880
        // retrieve the base revision
881
        $base_object = $reader->find($this->admin->getClass(), $id, $base_revision);
882
        if (!$base_object) {
883
            throw $this->createNotFoundException(
884
                sprintf(
885
                    'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
886
                    $id,
887
                    $base_revision,
888
                    $this->admin->getClass()
889
                )
890
            );
891
        }
892
893
        // retrieve the compare revision
894
        $compare_object = $reader->find($this->admin->getClass(), $id, $compare_revision);
895
        if (!$compare_object) {
896
            throw $this->createNotFoundException(
897
                sprintf(
898
                    'unable to find the targeted object `%s` from the revision `%s` with classname : `%s`',
899
                    $id,
900
                    $compare_revision,
901
                    $this->admin->getClass()
902
                )
903
            );
904
        }
905
906
        $this->admin->setSubject($base_object);
907
908
        // NEXT_MAJOR: Remove this line and use commented line below it instead
909
        $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...
910
        // $template = $this->templateRegistry->getTemplate('show_compare');
911
912
        return $this->renderWithExtraParams($template, [
913
            'action' => 'show',
914
            'object' => $base_object,
915
            'object_compare' => $compare_object,
916
            'elements' => $this->admin->getShow(),
917
        ], null);
918
    }
919
920
    /**
921
     * Export data to specified format.
922
     *
923
     * @throws AccessDeniedException If access is not granted
924
     * @throws \RuntimeException     If the export format is invalid
925
     *
926
     * @return Response
927
     */
928
    public function exportAction(Request $request)
929
    {
930
        $this->admin->checkAccess('export');
931
932
        $format = $request->get('format');
933
934
        // NEXT_MAJOR: remove the check
935
        if (!$this->has('sonata.admin.admin_exporter')) {
936
            @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...
937
                'Not registering the exporter bundle is deprecated since version 3.14.'
938
                .' You must register it to be able to use the export action in 4.0.',
939
                E_USER_DEPRECATED
940
            );
941
            $allowedExportFormats = (array) $this->admin->getExportFormats();
942
943
            $class = $this->admin->getClass();
944
            $filename = sprintf(
945
                'export_%s_%s.%s',
946
                strtolower(substr($class, strripos($class, '\\') + 1)),
947
                date('Y_m_d_H_i_s', strtotime('now')),
948
                $format
949
            );
950
            $exporter = $this->get('sonata.admin.exporter');
951
        } else {
952
            $adminExporter = $this->get('sonata.admin.admin_exporter');
953
            $allowedExportFormats = $adminExporter->getAvailableFormats($this->admin);
954
            $filename = $adminExporter->getExportFilename($this->admin, $format);
955
            $exporter = $this->get('sonata.exporter.exporter');
956
        }
957
958
        if (!\in_array($format, $allowedExportFormats)) {
959
            throw new \RuntimeException(
960
                sprintf(
961
                    'Export in format `%s` is not allowed for class: `%s`. Allowed formats are: `%s`',
962
                    $format,
963
                    $this->admin->getClass(),
964
                    implode(', ', $allowedExportFormats)
965
                )
966
            );
967
        }
968
969
        return $exporter->getResponse(
970
            $format,
971
            $filename,
972
            $this->admin->getDataSourceIterator()
973
        );
974
    }
975
976
    /**
977
     * Returns the Response object associated to the acl action.
978
     *
979
     * @param int|string|null $id
980
     *
981
     * @throws AccessDeniedException If access is not granted
982
     * @throws NotFoundHttpException If the object does not exist or the ACL is not enabled
983
     *
984
     * @return Response|RedirectResponse
985
     */
986
    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...
987
    {
988
        $request = $this->getRequest();
989
990
        if (!$this->admin->isAclEnabled()) {
991
            throw $this->createNotFoundException('ACL are not enabled for this admin');
992
        }
993
994
        $id = $request->get($this->admin->getIdParameter());
995
996
        $object = $this->admin->getObject($id);
997
998
        if (!$object) {
999
            throw $this->createNotFoundException(sprintf('unable to find the object with id: %s', $id));
1000
        }
1001
1002
        $this->admin->checkAccess('acl', $object);
1003
1004
        $this->admin->setSubject($object);
1005
        $aclUsers = $this->getAclUsers();
1006
        $aclRoles = $this->getAclRoles();
1007
1008
        $adminObjectAclManipulator = $this->get('sonata.admin.object.manipulator.acl.admin');
1009
        $adminObjectAclData = new AdminObjectAclData(
1010
            $this->admin,
1011
            $object,
1012
            $aclUsers,
1013
            $adminObjectAclManipulator->getMaskBuilderClass(),
1014
            $aclRoles
1015
        );
1016
1017
        $aclUsersForm = $adminObjectAclManipulator->createAclUsersForm($adminObjectAclData);
1018
        $aclRolesForm = $adminObjectAclManipulator->createAclRolesForm($adminObjectAclData);
1019
1020
        if ('POST' === $request->getMethod()) {
1021
            if ($request->request->has(AdminObjectAclManipulator::ACL_USERS_FORM_NAME)) {
1022
                $form = $aclUsersForm;
1023
                $updateMethod = 'updateAclUsers';
1024
            } elseif ($request->request->has(AdminObjectAclManipulator::ACL_ROLES_FORM_NAME)) {
1025
                $form = $aclRolesForm;
1026
                $updateMethod = 'updateAclRoles';
1027
            }
1028
1029
            if (isset($form)) {
1030
                $form->handleRequest($request);
1031
1032
                if ($form->isValid()) {
1033
                    $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...
1034
                    $this->addFlash(
1035
                        'sonata_flash_success',
1036
                        $this->trans('flash_acl_edit_success', [], 'SonataAdminBundle')
1037
                    );
1038
1039
                    return new RedirectResponse($this->admin->generateObjectUrl('acl', $object));
1040
                }
1041
            }
1042
        }
1043
1044
        // NEXT_MAJOR: Remove this line and use commented line below it instead
1045
        $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...
1046
        // $template = $this->templateRegistry->getTemplate('acl');
1047
1048
        return $this->renderWithExtraParams($template, [
1049
            'action' => 'acl',
1050
            'permissions' => $adminObjectAclData->getUserPermissions(),
1051
            'object' => $object,
1052
            'users' => $aclUsers,
1053
            'roles' => $aclRoles,
1054
            'aclUsersForm' => $aclUsersForm->createView(),
1055
            'aclRolesForm' => $aclRolesForm->createView(),
1056
        ], null);
1057
    }
1058
1059
    /**
1060
     * @return Request
1061
     */
1062
    public function getRequest()
1063
    {
1064
        return $this->container->get('request_stack')->getCurrentRequest();
1065
    }
1066
1067
    /**
1068
     * Gets a container configuration parameter by its name.
1069
     *
1070
     * @param string $name The parameter name
1071
     *
1072
     * @return mixed
1073
     */
1074
    protected function getParameter($name)
1075
    {
1076
        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_EnvParameters, 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...
1077
    }
1078
1079
    /**
1080
     * Render JSON.
1081
     *
1082
     * @param mixed $data
1083
     * @param int   $status
1084
     * @param array $headers
1085
     *
1086
     * @return Response with json encoded data
1087
     */
1088
    protected function renderJson($data, $status = 200, $headers = [])
1089
    {
1090
        return new JsonResponse($data, $status, $headers);
1091
    }
1092
1093
    /**
1094
     * Returns true if the request is a XMLHttpRequest.
1095
     *
1096
     * @return bool True if the request is an XMLHttpRequest, false otherwise
1097
     */
1098
    protected function isXmlHttpRequest()
1099
    {
1100
        $request = $this->getRequest();
1101
1102
        return $request->isXmlHttpRequest() || $request->get('_xml_http_request');
1103
    }
1104
1105
    /**
1106
     * Returns the correct RESTful verb, given either by the request itself or
1107
     * via the "_method" parameter.
1108
     *
1109
     * @return string HTTP method, either
1110
     */
1111
    protected function getRestMethod()
1112
    {
1113
        $request = $this->getRequest();
1114
1115
        if (Request::getHttpMethodParameterOverride() || !$request->request->has('_method')) {
1116
            return $request->getMethod();
1117
        }
1118
1119
        return $request->request->get('_method');
1120
    }
1121
1122
    /**
1123
     * Contextualize the admin class depends on the current request.
1124
     *
1125
     * @throws \RuntimeException
1126
     */
1127
    protected function configure(): void
1128
    {
1129
        $request = $this->getRequest();
1130
1131
        $adminCode = $request->get('_sonata_admin');
1132
1133
        if (!$adminCode) {
1134
            throw new \RuntimeException(sprintf(
1135
                'There is no `_sonata_admin` defined for the controller `%s` and the current route `%s`',
1136
                \get_class($this),
1137
                $request->get('_route')
1138
            ));
1139
        }
1140
1141
        $this->admin = $this->container->get('sonata.admin.pool')->getAdminByAdminCode($adminCode);
1142
1143
        if (!$this->admin) {
1144
            throw new \RuntimeException(sprintf(
1145
                'Unable to find the admin class related to the current controller (%s)',
1146
                \get_class($this)
1147
            ));
1148
        }
1149
1150
        $this->templateRegistry = $this->container->get($this->admin->getCode().'.template_registry');
1151
        if (!$this->templateRegistry instanceof TemplateRegistryInterface) {
1152
            throw new \RuntimeException(sprintf(
1153
                'Unable to find the template registry related to the current admin (%s)',
1154
                $this->admin->getCode()
1155
            ));
1156
        }
1157
1158
        $rootAdmin = $this->admin;
1159
1160
        while ($rootAdmin->isChild()) {
1161
            $rootAdmin->setCurrentChild(true);
1162
            $rootAdmin = $rootAdmin->getParent();
1163
        }
1164
1165
        $rootAdmin->setRequest($request);
1166
1167
        if ($request->get('uniqid')) {
1168
            $this->admin->setUniqid($request->get('uniqid'));
1169
        }
1170
    }
1171
1172
    /**
1173
     * Proxy for the logger service of the container.
1174
     * If no such service is found, a NullLogger is returned.
1175
     *
1176
     * @return LoggerInterface
1177
     */
1178
    protected function getLogger()
1179
    {
1180
        if ($this->container->has('logger')) {
1181
            $logger = $this->container->get('logger');
1182
            \assert($logger instanceof LoggerInterface);
1183
1184
            return $logger;
1185
        }
1186
1187
        return new NullLogger();
1188
    }
1189
1190
    /**
1191
     * Returns the base template name.
1192
     *
1193
     * @return string The template name
1194
     */
1195
    protected function getBaseTemplate()
1196
    {
1197
        if ($this->isXmlHttpRequest()) {
1198
            // NEXT_MAJOR: Remove this line and use commented line below it instead
1199
            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...
1200
            // return $this->templateRegistry->getTemplate('ajax');
1201
        }
1202
1203
        // NEXT_MAJOR: Remove this line and use commented line below it instead
1204
        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...
1205
        // return $this->templateRegistry->getTemplate('layout');
1206
    }
1207
1208
    /**
1209
     * @throws \Exception
1210
     */
1211
    protected function handleModelManagerException(\Exception $e): void
1212
    {
1213
        if ($this->get('kernel')->isDebug()) {
1214
            throw $e;
1215
        }
1216
1217
        $context = ['exception' => $e];
1218
        if ($e->getPrevious()) {
1219
            $context['previous_exception_message'] = $e->getPrevious()->getMessage();
1220
        }
1221
        $this->getLogger()->error($e->getMessage(), $context);
1222
    }
1223
1224
    /**
1225
     * Redirect the user depend on this choice.
1226
     *
1227
     * @param object $object
1228
     *
1229
     * @return RedirectResponse
1230
     */
1231
    protected function redirectTo($object)
1232
    {
1233
        $request = $this->getRequest();
1234
1235
        $url = false;
1236
1237
        if (null !== $request->get('btn_update_and_list')) {
1238
            return $this->redirectToList();
1239
        }
1240
        if (null !== $request->get('btn_create_and_list')) {
1241
            return $this->redirectToList();
1242
        }
1243
1244
        if (null !== $request->get('btn_create_and_create')) {
1245
            $params = [];
1246
            if ($this->admin->hasActiveSubClass()) {
1247
                $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...
1248
            }
1249
            $url = $this->admin->generateUrl('create', $params);
1250
        }
1251
1252
        if ('DELETE' === $this->getRestMethod()) {
1253
            return $this->redirectToList();
1254
        }
1255
1256
        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...
1257
            foreach (['edit', 'show'] as $route) {
1258
                if ($this->admin->hasRoute($route) && $this->admin->hasAccess($route, $object)) {
1259
                    $url = $this->admin->generateObjectUrl($route, $object);
1260
1261
                    break;
1262
                }
1263
            }
1264
        }
1265
1266
        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...
1267
            return $this->redirectToList();
1268
        }
1269
1270
        return new RedirectResponse($url);
1271
    }
1272
1273
    /**
1274
     * Redirects the user to the list view.
1275
     *
1276
     * @return RedirectResponse
1277
     */
1278
    final protected function redirectToList()
1279
    {
1280
        $parameters = [];
1281
1282
        if ($filter = $this->admin->getFilterParameters()) {
1283
            $parameters['filter'] = $filter;
1284
        }
1285
1286
        return $this->redirect($this->admin->generateUrl('list', $parameters));
1287
    }
1288
1289
    /**
1290
     * Returns true if the preview is requested to be shown.
1291
     *
1292
     * @return bool
1293
     */
1294
    protected function isPreviewRequested()
1295
    {
1296
        $request = $this->getRequest();
1297
1298
        return null !== $request->get('btn_preview');
1299
    }
1300
1301
    /**
1302
     * Returns true if the preview has been approved.
1303
     *
1304
     * @return bool
1305
     */
1306
    protected function isPreviewApproved()
1307
    {
1308
        $request = $this->getRequest();
1309
1310
        return null !== $request->get('btn_preview_approve');
1311
    }
1312
1313
    /**
1314
     * Returns true if the request is in the preview workflow.
1315
     *
1316
     * That means either a preview is requested or the preview has already been shown
1317
     * and it got approved/declined.
1318
     *
1319
     * @return bool
1320
     */
1321
    protected function isInPreviewMode()
1322
    {
1323
        return $this->admin->supportsPreviewMode()
1324
        && ($this->isPreviewRequested()
1325
            || $this->isPreviewApproved()
1326
            || $this->isPreviewDeclined());
1327
    }
1328
1329
    /**
1330
     * Returns true if the preview has been declined.
1331
     *
1332
     * @return bool
1333
     */
1334
    protected function isPreviewDeclined()
1335
    {
1336
        $request = $this->getRequest();
1337
1338
        return null !== $request->get('btn_preview_decline');
1339
    }
1340
1341
    /**
1342
     * Gets ACL users.
1343
     *
1344
     * @return \Traversable
1345
     */
1346
    protected function getAclUsers()
1347
    {
1348
        $aclUsers = [];
1349
1350
        $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_EnvParameters, 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...
1351
        if (null !== $userManagerServiceName && $this->has($userManagerServiceName)) {
1352
            $userManager = $this->get($userManagerServiceName);
1353
1354
            if (method_exists($userManager, 'findUsers')) {
1355
                $aclUsers = $userManager->findUsers();
1356
            }
1357
        }
1358
1359
        return \is_array($aclUsers) ? new \ArrayIterator($aclUsers) : $aclUsers;
1360
    }
1361
1362
    /**
1363
     * Gets ACL roles.
1364
     *
1365
     * @return \Traversable
1366
     */
1367
    protected function getAclRoles()
1368
    {
1369
        $aclRoles = [];
1370
        $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_EnvParameters, 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...
1371
        $pool = $this->container->get('sonata.admin.pool');
1372
1373
        foreach ($pool->getAdminServiceIds() as $id) {
1374
            try {
1375
                $admin = $pool->getInstance($id);
1376
            } catch (\Exception $e) {
1377
                continue;
1378
            }
1379
1380
            $baseRole = $admin->getSecurityHandler()->getBaseRole($admin);
1381
            foreach ($admin->getSecurityInformation() as $role => $permissions) {
1382
                $role = sprintf($baseRole, $role);
1383
                $aclRoles[] = $role;
1384
            }
1385
        }
1386
1387
        foreach ($roleHierarchy as $name => $roles) {
1388
            $aclRoles[] = $name;
1389
            $aclRoles = array_merge($aclRoles, $roles);
1390
        }
1391
1392
        $aclRoles = array_unique($aclRoles);
1393
1394
        return \is_array($aclRoles) ? new \ArrayIterator($aclRoles) : $aclRoles;
1395
    }
1396
1397
    /**
1398
     * Validate CSRF token for action without form.
1399
     *
1400
     * @param string $intention
1401
     *
1402
     * @throws HttpException
1403
     */
1404
    protected function validateCsrfToken($intention): void
1405
    {
1406
        $request = $this->getRequest();
1407
        $token = $request->request->get('_sonata_csrf_token', false);
1408
1409
        if ($this->container->has('security.csrf.token_manager')) {
1410
            $valid = $this->container->get('security.csrf.token_manager')->isTokenValid(new CsrfToken($intention, $token));
1411
        } else {
1412
            return;
1413
        }
1414
1415
        if (!$valid) {
1416
            throw new HttpException(400, 'The csrf token is not valid, CSRF attack?');
1417
        }
1418
    }
1419
1420
    /**
1421
     * Escape string for html output.
1422
     *
1423
     * @param string $s
1424
     *
1425
     * @return string
1426
     */
1427
    protected function escapeHtml($s)
1428
    {
1429
        return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
1430
    }
1431
1432
    /**
1433
     * Get CSRF token.
1434
     *
1435
     * @param string $intention
1436
     *
1437
     * @return string|false
1438
     */
1439
    protected function getCsrfToken($intention)
1440
    {
1441
        if ($this->container->has('security.csrf.token_manager')) {
1442
            return $this->container->get('security.csrf.token_manager')->getToken($intention)->getValue();
1443
        }
1444
1445
        return false;
1446
    }
1447
1448
    /**
1449
     * This method can be overloaded in your custom CRUD controller.
1450
     * It's called from createAction.
1451
     *
1452
     * @param mixed $object
1453
     *
1454
     * @return Response|null
1455
     */
1456
    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...
1457
    {
1458
    }
1459
1460
    /**
1461
     * This method can be overloaded in your custom CRUD controller.
1462
     * It's called from editAction.
1463
     *
1464
     * @param mixed $object
1465
     *
1466
     * @return Response|null
1467
     */
1468
    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...
1469
    {
1470
    }
1471
1472
    /**
1473
     * This method can be overloaded in your custom CRUD controller.
1474
     * It's called from deleteAction.
1475
     *
1476
     * @param mixed $object
1477
     *
1478
     * @return Response|null
1479
     */
1480
    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...
1481
    {
1482
    }
1483
1484
    /**
1485
     * This method can be overloaded in your custom CRUD controller.
1486
     * It's called from showAction.
1487
     *
1488
     * @param mixed $object
1489
     *
1490
     * @return Response|null
1491
     */
1492
    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...
1493
    {
1494
    }
1495
1496
    /**
1497
     * This method can be overloaded in your custom CRUD controller.
1498
     * It's called from listAction.
1499
     *
1500
     * @return Response|null
1501
     */
1502
    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...
1503
    {
1504
    }
1505
1506
    /**
1507
     * Translate a message id.
1508
     *
1509
     * @param string $id
1510
     * @param string $domain
1511
     * @param string $locale
1512
     *
1513
     * @return string translated string
1514
     */
1515
    final protected function trans($id, array $parameters = [], $domain = null, $locale = null)
1516
    {
1517
        $domain = $domain ?: $this->admin->getTranslationDomain();
1518
1519
        return $this->get('translator')->trans($id, $parameters, $domain, $locale);
1520
    }
1521
1522
    private function checkParentChildAssociation(Request $request, $object): void
1523
    {
1524
        if (!($parentAdmin = $this->admin->getParent())) {
1525
            return;
1526
        }
1527
1528
        // NEXT_MAJOR: remove this check
1529
        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...
1530
            return;
1531
        }
1532
1533
        $parentId = $request->get($parentAdmin->getIdParameter());
1534
1535
        $propertyAccessor = PropertyAccess::createPropertyAccessor();
1536
        $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...
1537
1538
        if ($parentAdmin->getObject($parentId) !== $propertyAccessor->getValue($object, $propertyPath)) {
1539
            // NEXT_MAJOR: make this exception
1540
            @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...
1541
                ." and won't be allowed in 4.0.",
1542
                E_USER_DEPRECATED
1543
            );
1544
        }
1545
    }
1546
1547
    /**
1548
     * Sets the admin form theme to form view. Used for compatibility between Symfony versions.
1549
     */
1550
    private function setFormTheme(FormView $formView, array $theme = null): void
1551
    {
1552
        $twig = $this->get('twig');
1553
1554
        // BC for Symfony < 3.2 where this runtime does not exists
1555
        if (!method_exists(AppVariable::class, 'getToken')) {
1556
            $twig->getExtension(FormExtension::class)->renderer->setTheme($formView, $theme);
1557
1558
            return;
1559
        }
1560
1561
        // BC for Symfony < 3.4 where runtime should be TwigRenderer
1562
        if (!method_exists(DebugCommand::class, 'getLoaderPaths')) {
1563
            $twig->getRuntime(TwigRenderer::class)->setTheme($formView, $theme);
1564
1565
            return;
1566
        }
1567
1568
        $twig->getRuntime(FormRenderer::class)->setTheme($formView, $theme);
1569
    }
1570
}
1571