Completed
Push — master ( a4094f...b681a2 )
by Grégoire
16s queued 12s
created

AbstractAdmin::getUrlSafeIdentifier()   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
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\Admin;
15
16
use Doctrine\Common\Util\ClassUtils;
17
use Knp\Menu\FactoryInterface;
18
use Knp\Menu\ItemInterface;
19
use Sonata\AdminBundle\Builder\DatagridBuilderInterface;
20
use Sonata\AdminBundle\Builder\FormContractorInterface;
21
use Sonata\AdminBundle\Builder\ListBuilderInterface;
22
use Sonata\AdminBundle\Builder\RouteBuilderInterface;
23
use Sonata\AdminBundle\Builder\ShowBuilderInterface;
24
use Sonata\AdminBundle\Datagrid\DatagridInterface;
25
use Sonata\AdminBundle\Datagrid\DatagridMapper;
26
use Sonata\AdminBundle\Datagrid\ListMapper;
27
use Sonata\AdminBundle\Datagrid\Pager;
28
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
29
use Sonata\AdminBundle\Filter\Persister\FilterPersisterInterface;
30
use Sonata\AdminBundle\Form\FormMapper;
31
use Sonata\AdminBundle\Form\Type\ModelHiddenType;
32
use Sonata\AdminBundle\Model\ModelManagerInterface;
33
use Sonata\AdminBundle\Object\Metadata;
34
use Sonata\AdminBundle\Route\RouteCollection;
35
use Sonata\AdminBundle\Route\RouteGeneratorInterface;
36
use Sonata\AdminBundle\Security\Handler\AclSecurityHandlerInterface;
37
use Sonata\AdminBundle\Security\Handler\SecurityHandlerInterface;
38
use Sonata\AdminBundle\Show\ShowMapper;
39
use Sonata\AdminBundle\Templating\MutableTemplateRegistryInterface;
40
use Sonata\AdminBundle\Translator\LabelTranslatorStrategyInterface;
41
use Sonata\Form\Validator\Constraints\InlineConstraint;
42
use Sonata\Form\Validator\ErrorElement;
43
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
44
use Symfony\Component\Form\Form;
45
use Symfony\Component\Form\FormBuilderInterface;
46
use Symfony\Component\Form\FormEvent;
47
use Symfony\Component\Form\FormEvents;
48
use Symfony\Component\HttpFoundation\Request;
49
use Symfony\Component\PropertyAccess\PropertyPath;
50
use Symfony\Component\Routing\Generator\UrlGeneratorInterface as RoutingUrlGeneratorInterface;
51
use Symfony\Component\Security\Acl\Model\DomainObjectInterface;
52
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
53
use Symfony\Component\Translation\TranslatorInterface;
54
use Symfony\Component\Validator\Validator\ValidatorInterface;
55
56
/**
57
 * @author Thomas Rabaix <[email protected]>
58
 */
59
abstract class AbstractAdmin implements AdminInterface, DomainObjectInterface, AdminTreeInterface
60
{
61
    public const CONTEXT_MENU = 'menu';
62
    public const CONTEXT_DASHBOARD = 'dashboard';
63
64
    public const CLASS_REGEX =
65
        '@
66
        (?:([A-Za-z0-9]*)\\\)?        # vendor name / app name
67
        (Bundle\\\)?                  # optional bundle directory
68
        ([A-Za-z0-9]+?)(?:Bundle)?\\\ # bundle name, with optional suffix
69
        (
70
            Entity|Document|Model|PHPCR|CouchDocument|Phpcr|
71
            Doctrine\\\Orm|Doctrine\\\Phpcr|Doctrine\\\MongoDB|Doctrine\\\CouchDB
72
        )\\\(.*)@x';
73
74
    public const MOSAIC_ICON_CLASS = 'fa fa-th-large fa-fw';
75
76
    /**
77
     * The list FieldDescription constructed from the configureListField method.
78
     *
79
     * @var array
80
     */
81
    protected $listFieldDescriptions = [];
82
83
    /**
84
     * The show FieldDescription constructed from the configureShowFields method.
85
     *
86
     * @var array
87
     */
88
    protected $showFieldDescriptions = [];
89
90
    /**
91
     * The list FieldDescription constructed from the configureFormField method.
92
     *
93
     * @var array
94
     */
95
    protected $formFieldDescriptions = [];
96
97
    /**
98
     * The filter FieldDescription constructed from the configureFilterField method.
99
     *
100
     * @var array
101
     */
102
    protected $filterFieldDescriptions = [];
103
104
    /**
105
     * The number of result to display in the list.
106
     *
107
     * @var int
108
     */
109
    protected $maxPerPage = 32;
110
111
    /**
112
     * The maximum number of page numbers to display in the list.
113
     *
114
     * @var int
115
     */
116
    protected $maxPageLinks = 25;
117
118
    /**
119
     * The base route name used to generate the routing information.
120
     *
121
     * @var string
122
     */
123
    protected $baseRouteName;
124
125
    /**
126
     * The base route pattern used to generate the routing information.
127
     *
128
     * @var string
129
     */
130
    protected $baseRoutePattern;
131
132
    /**
133
     * The base name controller used to generate the routing information.
134
     *
135
     * @var string
136
     */
137
    protected $baseControllerName;
138
139
    /**
140
     * The label class name  (used in the title/breadcrumb ...).
141
     *
142
     * @var string
143
     */
144
    protected $classnameLabel;
145
146
    /**
147
     * The translation domain to be used to translate messages.
148
     *
149
     * @var string
150
     */
151
    protected $translationDomain = 'messages';
152
153
    /**
154
     * Options to set to the form (ie, validation_groups).
155
     *
156
     * @var array
157
     */
158
    protected $formOptions = [];
159
160
    /**
161
     * Default values to the datagrid.
162
     *
163
     * @var array
164
     */
165
    protected $datagridValues = [
166
        '_page' => 1,
167
        '_per_page' => 32,
168
    ];
169
170
    /**
171
     * Predefined per page options.
172
     *
173
     * @var array
174
     */
175
    protected $perPageOptions = [16, 32, 64, 128, 256];
176
177
    /**
178
     * Pager type.
179
     *
180
     * @var string
181
     */
182
    protected $pagerType = Pager::TYPE_DEFAULT;
183
184
    /**
185
     * The code related to the admin.
186
     *
187
     * @var string
188
     */
189
    protected $code;
190
191
    /**
192
     * The label.
193
     *
194
     * @var string
195
     */
196
    protected $label;
197
198
    /**
199
     * Whether or not to persist the filters in the session.
200
     *
201
     * NEXT_MAJOR: remove this property
202
     *
203
     * @var bool
204
     *
205
     * @deprecated since sonata-project/admin-bundle 3.34, to be removed in 4.0.
206
     */
207
    protected $persistFilters = false;
208
209
    /**
210
     * Array of routes related to this admin.
211
     *
212
     * @var RouteCollection
213
     */
214
    protected $routes;
215
216
    /**
217
     * The subject only set in edit/update/create mode.
218
     *
219
     * @var object|null
220
     */
221
    protected $subject;
222
223
    /**
224
     * Define a Collection of child admin, ie /admin/order/{id}/order-element/{childId}.
225
     *
226
     * @var array
227
     */
228
    protected $children = [];
229
230
    /**
231
     * Reference the parent admin.
232
     *
233
     * @var AdminInterface|null
234
     */
235
    protected $parent;
236
237
    /**
238
     * The related parent association, ie if OrderElement has a parent property named order,
239
     * then the $parentAssociationMapping must be a string named `order`.
240
     *
241
     * NEXT_MAJOR: remove this attribute.
242
     *
243
     * @deprecated This attribute is deprecated since sonata-project/admin-bundle 3.24 and will be removed in 4.0
244
     *
245
     * @var string
246
     */
247
    protected $baseCodeRoute = '';
248
249
    /**
250
     * NEXT_MAJOR: should be default array and private.
251
     *
252
     * @var string|array
253
     */
254
    protected $parentAssociationMapping;
255
256
    /**
257
     * Reference the parent FieldDescription related to this admin
258
     * only set for FieldDescription which is associated to an Sub Admin instance.
259
     *
260
     * @var FieldDescriptionInterface
261
     */
262
    protected $parentFieldDescription;
263
264
    /**
265
     * If true then the current admin is part of the nested admin set (from the url).
266
     *
267
     * @var bool
268
     */
269
    protected $currentChild = false;
270
271
    /**
272
     * The uniqid is used to avoid clashing with 2 admin related to the code
273
     * ie: a Block linked to a Block.
274
     *
275
     * @var string
276
     */
277
    protected $uniqid;
278
279
    /**
280
     * The Entity or Document manager.
281
     *
282
     * @var ModelManagerInterface
283
     */
284
    protected $modelManager;
285
286
    /**
287
     * The current request object.
288
     *
289
     * @var Request|null
290
     */
291
    protected $request;
292
293
    /**
294
     * The translator component.
295
     *
296
     * NEXT_MAJOR: remove this property
297
     *
298
     * @var \Symfony\Component\Translation\TranslatorInterface
299
     *
300
     * @deprecated since sonata-project/admin-bundle 3.9, to be removed with 4.0
301
     */
302
    protected $translator;
303
304
    /**
305
     * The related form contractor.
306
     *
307
     * @var FormContractorInterface
308
     */
309
    protected $formContractor;
310
311
    /**
312
     * The related list builder.
313
     *
314
     * @var ListBuilderInterface
315
     */
316
    protected $listBuilder;
317
318
    /**
319
     * The related view builder.
320
     *
321
     * @var ShowBuilderInterface
322
     */
323
    protected $showBuilder;
324
325
    /**
326
     * The related datagrid builder.
327
     *
328
     * @var DatagridBuilderInterface
329
     */
330
    protected $datagridBuilder;
331
332
    /**
333
     * @var RouteBuilderInterface
334
     */
335
    protected $routeBuilder;
336
337
    /**
338
     * The datagrid instance.
339
     *
340
     * @var DatagridInterface|null
341
     */
342
    protected $datagrid;
343
344
    /**
345
     * The router instance.
346
     *
347
     * @var RouteGeneratorInterface|null
348
     */
349
    protected $routeGenerator;
350
351
    /**
352
     * @var SecurityHandlerInterface
353
     */
354
    protected $securityHandler;
355
356
    /**
357
     * @var ValidatorInterface
358
     */
359
    protected $validator;
360
361
    /**
362
     * The configuration pool.
363
     *
364
     * @var Pool
365
     */
366
    protected $configurationPool;
367
368
    /**
369
     * @var ItemInterface
370
     */
371
    protected $menu;
372
373
    /**
374
     * @var FactoryInterface
375
     */
376
    protected $menuFactory;
377
378
    /**
379
     * @var array<string, bool>
380
     */
381
    protected $loaded = [
382
        'view_fields' => false,
383
        'view_groups' => false,
384
        'routes' => false,
385
        'tab_menu' => false,
386
    ];
387
388
    /**
389
     * @var string[]
390
     */
391
    protected $formTheme = [];
392
393
    /**
394
     * @var string[]
395
     */
396
    protected $filterTheme = [];
397
398
    /**
399
     * @var AdminExtensionInterface[]
400
     */
401
    protected $extensions = [];
402
403
    /**
404
     * @var LabelTranslatorStrategyInterface
405
     */
406
    protected $labelTranslatorStrategy;
407
408
    /**
409
     * Setting to true will enable preview mode for
410
     * the entity and show a preview button in the
411
     * edit/create forms.
412
     *
413
     * @var bool
414
     */
415
    protected $supportsPreviewMode = false;
416
417
    /**
418
     * Roles and permissions per role.
419
     *
420
     * @var array 'role' => ['permission', 'permission']
421
     */
422
    protected $securityInformation = [];
423
424
    protected $cacheIsGranted = [];
425
426
    /**
427
     * Action list for the search result.
428
     *
429
     * @var string[]
430
     */
431
    protected $searchResultActions = ['edit', 'show'];
432
433
    protected $listModes = [
434
        'list' => [
435
            'class' => 'fa fa-list fa-fw',
436
        ],
437
        'mosaic' => [
438
            'class' => self::MOSAIC_ICON_CLASS,
439
        ],
440
    ];
441
442
    /**
443
     * The Access mapping.
444
     *
445
     * @var array [action1 => requiredRole1, action2 => [requiredRole2, requiredRole3]]
446
     */
447
    protected $accessMapping = [];
448
449
    /**
450
     * @var MutableTemplateRegistryInterface
451
     */
452
    private $templateRegistry;
453
454
    /**
455
     * The class name managed by the admin class.
456
     *
457
     * @var string
458
     */
459
    private $class;
460
461
    /**
462
     * The subclasses supported by the admin class.
463
     *
464
     * @var array<string, string>
465
     */
466
    private $subClasses = [];
467
468
    /**
469
     * The list collection.
470
     *
471
     * @var FieldDescriptionCollection
472
     */
473
    private $list;
474
475
    /**
476
     * @var FieldDescriptionCollection|null
477
     */
478
    private $show;
479
480
    /**
481
     * @var Form|null
482
     */
483
    private $form;
484
485
    /**
486
     * The cached base route name.
487
     *
488
     * @var string
489
     */
490
    private $cachedBaseRouteName;
491
492
    /**
493
     * The cached base route pattern.
494
     *
495
     * @var string
496
     */
497
    private $cachedBaseRoutePattern;
498
499
    /**
500
     * The form group disposition.
501
     *
502
     * NEXT_MAJOR: must have `[]` as default value and remove the possibility to
503
     * hold boolean values.
504
     *
505
     * @var array|bool
506
     */
507
    private $formGroups = false;
508
509
    /**
510
     * The form tabs disposition.
511
     *
512
     * NEXT_MAJOR: must have `[]` as default value and remove the possibility to
513
     * hold boolean values.
514
     *
515
     * @var array|bool
516
     */
517
    private $formTabs = false;
518
519
    /**
520
     * The view group disposition.
521
     *
522
     * NEXT_MAJOR: must have `[]` as default value and remove the possibility to
523
     * hold boolean values.
524
     *
525
     * @var array|bool
526
     */
527
    private $showGroups = false;
528
529
    /**
530
     * The view tab disposition.
531
     *
532
     * NEXT_MAJOR: must have `[]` as default value and remove the possibility to
533
     * hold boolean values.
534
     *
535
     * @var array|bool
536
     */
537
    private $showTabs = false;
538
539
    /**
540
     * The manager type to use for the admin.
541
     *
542
     * @var string
543
     */
544
    private $managerType;
545
546
    /**
547
     * Component responsible for persisting filters.
548
     *
549
     * @var FilterPersisterInterface|null
550
     */
551
    private $filterPersister;
552
553
    /**
554
     * @param string      $code
555
     * @param string      $class
556
     * @param string|null $baseControllerName
557
     */
558
    public function __construct($code, $class, $baseControllerName = null)
559
    {
560
        if (!\is_string($code)) {
561
            @trigger_error(sprintf(
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...
562
                'Passing other type than string as argument 1 for method %s() is deprecated since sonata-project/admin-bundle 3.65. It will accept only string in version 4.0.',
563
                __METHOD__
564
            ), E_USER_DEPRECATED);
565
        }
566
        $this->code = $code;
567
        if (!\is_string($class)) {
568
            @trigger_error(sprintf(
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...
569
                'Passing other type than string as argument 2 for method %s() is deprecated since sonata-project/admin-bundle 3.65. It will accept only string in version 4.0.',
570
                __METHOD__
571
            ), E_USER_DEPRECATED);
572
        }
573
        $this->class = $class;
574
        if (null !== $baseControllerName && !\is_string($baseControllerName)) {
575
            @trigger_error(sprintf(
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...
576
                'Passing other type than string or null as argument 3 for method %s() is deprecated since sonata-project/admin-bundle 3.65. It will accept only string and null in version 4.0.',
577
                __METHOD__
578
            ), E_USER_DEPRECATED);
579
        }
580
        $this->baseControllerName = $baseControllerName;
581
582
        $this->predefinePerPageOptions();
583
        $this->datagridValues['_per_page'] = $this->maxPerPage;
584
    }
585
586
    /**
587
     * {@inheritdoc}
588
     */
589
    public function getExportFormats()
590
    {
591
        return [
592
            'json', 'xml', 'csv', 'xls',
593
        ];
594
    }
595
596
    /**
597
     * {@inheritdoc}
598
     */
599
    public function getExportFields(): array
600
    {
601
        $fields = $this->getModelManager()->getExportFields($this->getClass());
602
603
        foreach ($this->getExtensions() as $extension) {
604
            if (method_exists($extension, 'configureExportFields')) {
605
                $fields = $extension->configureExportFields($this, $fields);
606
            }
607
        }
608
609
        return $fields;
610
    }
611
612
    public function getDataSourceIterator()
613
    {
614
        $datagrid = $this->getDatagrid();
615
        $datagrid->buildPager();
616
617
        $fields = [];
618
619
        foreach ($this->getExportFields() as $key => $field) {
620
            $label = $this->getTranslationLabel($field, 'export', 'label');
621
            $transLabel = $this->trans($label);
622
623
            // NEXT_MAJOR: Remove this hack, because all field labels will be translated with the major release
624
            // No translation key exists
625
            if ($transLabel === $label) {
626
                $fields[$key] = $field;
627
            } else {
628
                $fields[$transLabel] = $field;
629
            }
630
        }
631
632
        return $this->getModelManager()->getDataSourceIterator($datagrid, $fields);
0 ignored issues
show
Bug introduced by
It seems like $datagrid defined by $this->getDatagrid() on line 614 can be null; however, Sonata\AdminBundle\Model...getDataSourceIterator() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
633
    }
634
635
    public function validate(ErrorElement $errorElement, $object): void
636
    {
637
    }
638
639
    /**
640
     * define custom variable.
641
     */
642
    public function initialize(): void
643
    {
644
        if (!$this->classnameLabel) {
645
            /* NEXT_MAJOR: remove cast to string, null is not supposed to be
646
            supported but was documented as such */
647
            $this->classnameLabel = substr(
648
                (string) $this->getClass(),
649
                strrpos((string) $this->getClass(), '\\') + 1
650
            );
651
        }
652
653
        $this->configure();
654
    }
655
656
    public function update($object)
657
    {
658
        $this->preUpdate($object);
659
        foreach ($this->extensions as $extension) {
660
            $extension->preUpdate($this, $object);
661
        }
662
663
        $result = $this->getModelManager()->update($object);
664
        // BC compatibility
665
        if (null !== $result) {
666
            $object = $result;
667
        }
668
669
        $this->postUpdate($object);
670
        foreach ($this->extensions as $extension) {
671
            $extension->postUpdate($this, $object);
672
        }
673
674
        return $object;
675
    }
676
677
    public function create($object)
678
    {
679
        $this->prePersist($object);
680
        foreach ($this->extensions as $extension) {
681
            $extension->prePersist($this, $object);
682
        }
683
684
        $result = $this->getModelManager()->create($object);
685
        // BC compatibility
686
        if (null !== $result) {
687
            $object = $result;
688
        }
689
690
        $this->postPersist($object);
691
        foreach ($this->extensions as $extension) {
692
            $extension->postPersist($this, $object);
693
        }
694
695
        $this->createObjectSecurity($object);
696
697
        return $object;
698
    }
699
700
    public function delete($object): void
701
    {
702
        $this->preRemove($object);
703
        foreach ($this->extensions as $extension) {
704
            $extension->preRemove($this, $object);
705
        }
706
707
        $this->getSecurityHandler()->deleteObjectSecurity($this, $object);
708
        $this->getModelManager()->delete($object);
709
710
        $this->postRemove($object);
711
        foreach ($this->extensions as $extension) {
712
            $extension->postRemove($this, $object);
713
        }
714
    }
715
716
    public function preValidate(object $object): void
717
    {
718
    }
719
720
    public function preUpdate($object): void
721
    {
722
    }
723
724
    public function postUpdate($object): void
725
    {
726
    }
727
728
    public function prePersist($object): void
729
    {
730
    }
731
732
    public function postPersist($object): void
733
    {
734
    }
735
736
    public function preRemove($object): void
737
    {
738
    }
739
740
    public function postRemove($object): void
741
    {
742
    }
743
744
    public function preBatchAction($actionName, ProxyQueryInterface $query, array &$idx, $allElements): void
745
    {
746
    }
747
748
    public function getFilterParameters()
749
    {
750
        $parameters = [];
751
752
        // build the values array
753
        if ($this->hasRequest()) {
754
            $filters = $this->request->query->get('filter', []);
755
            if (isset($filters['_page'])) {
756
                $filters['_page'] = (int) $filters['_page'];
757
            }
758
            if (isset($filters['_per_page'])) {
759
                $filters['_per_page'] = (int) $filters['_per_page'];
760
            }
761
762
            // if filter persistence is configured
763
            // NEXT_MAJOR: remove `$this->persistFilters !== false` from the condition
764
            if (false !== $this->persistFilters && null !== $this->filterPersister) {
0 ignored issues
show
Deprecated Code introduced by
The property Sonata\AdminBundle\Admin...tAdmin::$persistFilters has been deprecated with message: since sonata-project/admin-bundle 3.34, to be removed in 4.0.

This property 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 property will be removed from the class and what other property to use instead.

Loading history...
765
                // if reset filters is asked, remove from storage
766
                if ('reset' === $this->request->query->get('filters')) {
767
                    $this->filterPersister->reset($this->getCode());
768
                }
769
770
                // if no filters, fetch from storage
771
                // otherwise save to storage
772
                if (empty($filters)) {
773
                    $filters = $this->filterPersister->get($this->getCode());
774
                } else {
775
                    $this->filterPersister->set($this->getCode(), $filters);
776
                }
777
            }
778
779
            $parameters = array_merge(
780
                $this->getModelManager()->getDefaultSortValues($this->getClass()),
781
                $this->datagridValues,
782
                $this->getDefaultFilterValues(),
783
                $filters
784
            );
785
786
            if (!$this->determinedPerPageValue($parameters['_per_page'])) {
787
                $parameters['_per_page'] = $this->maxPerPage;
788
            }
789
790
            // always force the parent value
791
            if ($this->isChild() && $this->getParentAssociationMapping()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getParentAssociationMapping() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null 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...
792
                $name = str_replace('.', '__', $this->getParentAssociationMapping());
793
                $parameters[$name] = ['value' => $this->request->get($this->getParent()->getIdParameter())];
794
            }
795
        }
796
797
        return $parameters;
798
    }
799
800
    /**
801
     * Returns the name of the parent related field, so the field can be use to set the default
802
     * value (ie the parent object) or to filter the object.
803
     *
804
     * @throws \InvalidArgumentException
805
     *
806
     * @return string|null
807
     */
808
    public function getParentAssociationMapping()
809
    {
810
        // NEXT_MAJOR: remove array check
811
        if (\is_array($this->parentAssociationMapping) && $this->getParent()) {
812
            $parent = $this->getParent()->getCode();
813
814
            if (\array_key_exists($parent, $this->parentAssociationMapping)) {
815
                return $this->parentAssociationMapping[$parent];
816
            }
817
818
            throw new \InvalidArgumentException(sprintf(
819
                "There's no association between %s and %s.",
820
                $this->getCode(),
821
                $this->getParent()->getCode()
822
            ));
823
        }
824
825
        // NEXT_MAJOR: remove this line
826
        return $this->parentAssociationMapping;
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->parentAssociationMapping; of type string|array adds the type array to the return on line 826 which is incompatible with the return type documented by Sonata\AdminBundle\Admin...arentAssociationMapping of type string|null.
Loading history...
827
    }
828
829
    /**
830
     * @param string $code
831
     * @param string $value
832
     */
833
    final public function addParentAssociationMapping($code, $value): void
834
    {
835
        $this->parentAssociationMapping[$code] = $value;
836
    }
837
838
    /**
839
     * Returns the baseRoutePattern used to generate the routing information.
840
     *
841
     * @throws \RuntimeException
842
     *
843
     * @return string the baseRoutePattern used to generate the routing information
844
     */
845
    public function getBaseRoutePattern()
846
    {
847
        if (null !== $this->cachedBaseRoutePattern) {
848
            return $this->cachedBaseRoutePattern;
849
        }
850
851
        if ($this->isChild()) { // the admin class is a child, prefix it with the parent route pattern
852
            $baseRoutePattern = $this->baseRoutePattern;
853
            if (!$this->baseRoutePattern) {
854
                preg_match(self::CLASS_REGEX, $this->class, $matches);
855
856
                if (!$matches) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $matches of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
857
                    throw new \RuntimeException(sprintf('Please define a default `baseRoutePattern` value for the admin class `%s`', static::class));
858
                }
859
                $baseRoutePattern = $this->urlize($matches[5], '-');
860
            }
861
862
            $this->cachedBaseRoutePattern = sprintf(
863
                '%s/%s/%s',
864
                $this->getParent()->getBaseRoutePattern(),
865
                $this->getParent()->getRouterIdParameter(),
866
                $baseRoutePattern
867
            );
868
        } elseif ($this->baseRoutePattern) {
869
            $this->cachedBaseRoutePattern = $this->baseRoutePattern;
870
        } else {
871
            preg_match(self::CLASS_REGEX, $this->class, $matches);
872
873
            if (!$matches) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $matches of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
874
                throw new \RuntimeException(sprintf('Please define a default `baseRoutePattern` value for the admin class `%s`', static::class));
875
            }
876
877
            $this->cachedBaseRoutePattern = sprintf(
878
                '/%s%s/%s',
879
                empty($matches[1]) ? '' : $this->urlize($matches[1], '-').'/',
880
                $this->urlize($matches[3], '-'),
881
                $this->urlize($matches[5], '-')
882
            );
883
        }
884
885
        return $this->cachedBaseRoutePattern;
886
    }
887
888
    /**
889
     * Returns the baseRouteName used to generate the routing information.
890
     *
891
     * @throws \RuntimeException
892
     *
893
     * @return string the baseRouteName used to generate the routing information
894
     */
895
    public function getBaseRouteName()
896
    {
897
        if (null !== $this->cachedBaseRouteName) {
898
            return $this->cachedBaseRouteName;
899
        }
900
901
        if ($this->isChild()) { // the admin class is a child, prefix it with the parent route name
902
            $baseRouteName = $this->baseRouteName;
903
            if (!$this->baseRouteName) {
904
                preg_match(self::CLASS_REGEX, $this->class, $matches);
905
906
                if (!$matches) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $matches of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
907
                    throw new \RuntimeException(sprintf('Cannot automatically determine base route name, please define a default `baseRouteName` value for the admin class `%s`', static::class));
908
                }
909
                $baseRouteName = $this->urlize($matches[5]);
910
            }
911
912
            $this->cachedBaseRouteName = sprintf(
913
                '%s_%s',
914
                $this->getParent()->getBaseRouteName(),
915
                $baseRouteName
916
            );
917
        } elseif ($this->baseRouteName) {
918
            $this->cachedBaseRouteName = $this->baseRouteName;
919
        } else {
920
            preg_match(self::CLASS_REGEX, $this->class, $matches);
921
922
            if (!$matches) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $matches of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
923
                throw new \RuntimeException(sprintf('Cannot automatically determine base route name, please define a default `baseRouteName` value for the admin class `%s`', static::class));
924
            }
925
926
            $this->cachedBaseRouteName = sprintf(
927
                'admin_%s%s_%s',
928
                empty($matches[1]) ? '' : $this->urlize($matches[1]).'_',
929
                $this->urlize($matches[3]),
930
                $this->urlize($matches[5])
931
            );
932
        }
933
934
        return $this->cachedBaseRouteName;
935
    }
936
937
    public function getClass()
938
    {
939
        if ($this->hasActiveSubClass()) {
940
            if ($this->getParentFieldDescription()) {
941
                throw new \RuntimeException('Feature not implemented: an embedded admin cannot have subclass');
942
            }
943
944
            $subClass = $this->getRequest()->query->get('subclass');
945
946
            if (!$this->hasSubClass($subClass)) {
947
                throw new \RuntimeException(sprintf('Subclass "%s" is not defined.', $subClass));
948
            }
949
950
            return $this->getSubClass($subClass);
951
        }
952
953
        // see https://github.com/sonata-project/SonataCoreBundle/commit/247eeb0a7ca7211142e101754769d70bc402a5b4
954
        if ($this->subject && \is_object($this->subject)) {
955
            return ClassUtils::getClass($this->subject);
956
        }
957
958
        return $this->class;
959
    }
960
961
    public function getSubClasses(): array
962
    {
963
        return $this->subClasses;
964
    }
965
966
    /**
967
     * NEXT_MAJOR: remove this method.
968
     */
969
    public function addSubClass($subClass): void
970
    {
971
        @trigger_error(sprintf(
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...
972
            'Method "%s" is deprecated since sonata-project/admin-bundle 3.30 and will be removed in 4.0.',
973
            __METHOD__
974
        ), E_USER_DEPRECATED);
975
976
        if (!\in_array($subClass, $this->subClasses, true)) {
977
            $this->subClasses[] = $subClass;
978
        }
979
    }
980
981
    public function setSubClasses(array $subClasses): void
982
    {
983
        $this->subClasses = $subClasses;
984
    }
985
986
    public function hasSubClass($name)
987
    {
988
        return isset($this->subClasses[$name]);
989
    }
990
991
    public function hasActiveSubClass()
992
    {
993
        if (\count($this->subClasses) > 0 && $this->request) {
994
            return null !== $this->getRequest()->query->get('subclass');
995
        }
996
997
        return false;
998
    }
999
1000
    public function getActiveSubClass()
1001
    {
1002
        if (!$this->hasActiveSubClass()) {
1003
            @trigger_error(sprintf(
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...
1004
                'Calling %s() when there is no active subclass is deprecated since sonata-project/admin-bundle 3.52 and will throw an exception in 4.0. '.
1005
                'Use %s::hasActiveSubClass() to know if there is an active subclass.',
1006
                __METHOD__,
1007
                __CLASS__
1008
            ), E_USER_DEPRECATED);
1009
            // NEXT_MAJOR : remove the previous `trigger_error()` call, the `return null` statement, uncomment the following exception and declare string as return type
1010
            // throw new \LogicException(sprintf(
1011
            //    'Admin "%s" has no active subclass.',
1012
            //    static::class
1013
            // ));
1014
1015
            return null;
1016
        }
1017
1018
        return $this->getSubClass($this->getActiveSubclassCode());
1019
    }
1020
1021
    public function getActiveSubclassCode()
1022
    {
1023
        if (!$this->hasActiveSubClass()) {
1024
            @trigger_error(sprintf(
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...
1025
                'Calling %s() when there is no active subclass is deprecated since sonata-project/admin-bundle 3.52 and will throw an exception in 4.0. '.
1026
                'Use %s::hasActiveSubClass() to know if there is an active subclass.',
1027
                __METHOD__,
1028
                __CLASS__
1029
            ), E_USER_DEPRECATED);
1030
            // NEXT_MAJOR : remove the previous `trigger_error()` call, the `return null` statement, uncomment the following exception and declare string as return type
1031
            // throw new \LogicException(sprintf(
1032
            //    'Admin "%s" has no active subclass.',
1033
            //    static::class
1034
            // ));
1035
1036
            return null;
1037
        }
1038
1039
        $subClass = $this->getRequest()->query->get('subclass');
1040
1041
        if (!$this->hasSubClass($subClass)) {
1042
            @trigger_error(sprintf(
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...
1043
                'Calling %s() when there is no active subclass is deprecated since sonata-project/admin-bundle 3.52 and will throw an exception in 4.0. '.
1044
                'Use %s::hasActiveSubClass() to know if there is an active subclass.',
1045
                __METHOD__,
1046
                __CLASS__
1047
            ), E_USER_DEPRECATED);
1048
            // NEXT_MAJOR : remove the previous `trigger_error()` call, the `return null` statement, uncomment the following exception and declare string as return type
1049
            // throw new \LogicException(sprintf(
1050
            //    'Admin "%s" has no active subclass.',
1051
            //    static::class
1052
            // ));
1053
1054
            return null;
1055
        }
1056
1057
        return $subClass;
1058
    }
1059
1060
    public function getBatchActions()
1061
    {
1062
        $actions = [];
1063
1064
        if ($this->hasRoute('delete') && $this->hasAccess('delete')) {
1065
            $actions['delete'] = [
1066
                'label' => 'action_delete',
1067
                'translation_domain' => 'SonataAdminBundle',
1068
                'ask_confirmation' => true, // by default always true
1069
            ];
1070
        }
1071
1072
        $actions = $this->configureBatchActions($actions);
1073
1074
        foreach ($this->getExtensions() as $extension) {
1075
            $actions = $extension->configureBatchActions($this, $actions);
1076
        }
1077
1078
        foreach ($actions  as $name => &$action) {
1079
            if (!\array_key_exists('label', $action)) {
1080
                $action['label'] = $this->getTranslationLabel($name, 'batch', 'label');
1081
            }
1082
1083
            if (!\array_key_exists('translation_domain', $action)) {
1084
                $action['translation_domain'] = $this->getTranslationDomain();
1085
            }
1086
        }
1087
1088
        return $actions;
1089
    }
1090
1091
    public function getRoutes()
1092
    {
1093
        $this->buildRoutes();
1094
1095
        return $this->routes;
1096
    }
1097
1098
    public function getRouterIdParameter()
1099
    {
1100
        return '{'.$this->getIdParameter().'}';
1101
    }
1102
1103
    public function getIdParameter()
1104
    {
1105
        $parameter = 'id';
1106
1107
        for ($i = 0; $i < $this->getChildDepth(); ++$i) {
1108
            $parameter = 'child'.ucfirst($parameter);
1109
        }
1110
1111
        return $parameter;
1112
    }
1113
1114
    public function hasRoute($name)
1115
    {
1116
        if (!$this->routeGenerator) {
1117
            throw new \RuntimeException('RouteGenerator cannot be null');
1118
        }
1119
1120
        return $this->routeGenerator->hasAdminRoute($this, $name);
1121
    }
1122
1123
    public function isCurrentRoute(string $name, ?string $adminCode = null): bool
1124
    {
1125
        if (!$this->hasRequest()) {
1126
            return false;
1127
        }
1128
1129
        $request = $this->getRequest();
1130
        $route = $request->get('_route');
1131
1132
        if ($adminCode) {
1133
            $admin = $this->getConfigurationPool()->getAdminByAdminCode($adminCode);
1134
        } else {
1135
            $admin = $this;
1136
        }
1137
1138
        if (!$admin) {
1139
            return false;
1140
        }
1141
1142
        return ($admin->getBaseRouteName().'_'.$name) === $route;
1143
    }
1144
1145
    public function generateObjectUrl($name, $object, array $parameters = [], $referenceType = RoutingUrlGeneratorInterface::ABSOLUTE_PATH)
1146
    {
1147
        $parameters['id'] = $this->getUrlSafeIdentifier($object);
1148
1149
        return $this->generateUrl($name, $parameters, $referenceType);
1150
    }
1151
1152
    public function generateUrl($name, array $parameters = [], $referenceType = RoutingUrlGeneratorInterface::ABSOLUTE_PATH)
1153
    {
1154
        return $this->routeGenerator->generateUrl($this, $name, $parameters, $referenceType);
1155
    }
1156
1157
    public function generateMenuUrl($name, array $parameters = [], $referenceType = RoutingUrlGeneratorInterface::ABSOLUTE_PATH)
1158
    {
1159
        return $this->routeGenerator->generateMenuUrl($this, $name, $parameters, $referenceType);
1160
    }
1161
1162
    final public function setTemplateRegistry(MutableTemplateRegistryInterface $templateRegistry): void
1163
    {
1164
        $this->templateRegistry = $templateRegistry;
1165
    }
1166
1167
    /**
1168
     * @param array<string, string> $templates
1169
     */
1170
    public function setTemplates(array $templates): void
1171
    {
1172
        $this->getTemplateRegistry()->setTemplates($templates);
1173
    }
1174
1175
    /**
1176
     * {@inheritdoc}
1177
     */
1178
    public function setTemplate($name, $template): void
1179
    {
1180
        $this->getTemplateRegistry()->setTemplate($name, $template);
1181
    }
1182
1183
    public function getNewInstance()
1184
    {
1185
        $object = $this->getModelManager()->getModelInstance($this->getClass());
1186
        foreach ($this->getExtensions() as $extension) {
1187
            $extension->alterNewInstance($this, $object);
1188
        }
1189
1190
        return $object;
1191
    }
1192
1193
    public function getFormBuilder()
1194
    {
1195
        $this->formOptions['data_class'] = $this->getClass();
1196
1197
        $formBuilder = $this->getFormContractor()->getFormBuilder(
1198
            $this->getUniqid(),
1199
            $this->formOptions
1200
        );
1201
1202
        $this->defineFormBuilder($formBuilder);
1203
1204
        return $formBuilder;
1205
    }
1206
1207
    /**
1208
     * This method is being called by the main admin class and the child class,
1209
     * the getFormBuilder is only call by the main admin class.
1210
     */
1211
    public function defineFormBuilder(FormBuilderInterface $formBuilder): void
1212
    {
1213
        if (!$this->hasSubject()) {
1214
            @trigger_error(sprintf(
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...
1215
                'Calling %s() when there is no subject is deprecated since sonata-project/admin-bundle 3.65 and will throw an exception in 4.0. '.
1216
                'Use %s::setSubject() to set the subject.',
1217
                __METHOD__,
1218
                __CLASS__
1219
            ), E_USER_DEPRECATED);
1220
            // NEXT_MAJOR : remove the previous `trigger_error()` call and uncomment the following exception
1221
            // throw new \LogicException(sprintf(
1222
            //    'Admin "%s" has no subject.',
1223
            //    static::class
1224
            // ));
1225
        }
1226
1227
        $mapper = new FormMapper($this->getFormContractor(), $formBuilder, $this);
1228
1229
        $this->configureFormFields($mapper);
1230
1231
        foreach ($this->getExtensions() as $extension) {
1232
            $extension->configureFormFields($mapper);
1233
        }
1234
1235
        $this->attachInlineValidator();
1236
    }
1237
1238
    public function attachAdminClass(FieldDescriptionInterface $fieldDescription): void
1239
    {
1240
        $pool = $this->getConfigurationPool();
1241
1242
        $adminCode = $fieldDescription->getOption('admin_code');
1243
1244
        if (null !== $adminCode) {
1245
            $admin = $pool->getAdminByAdminCode($adminCode);
1246
        } else {
1247
            $admin = $pool->getAdminByClass($fieldDescription->getTargetEntity());
1248
        }
1249
1250
        if (!$admin) {
1251
            return;
1252
        }
1253
1254
        if ($this->hasRequest()) {
1255
            $admin->setRequest($this->getRequest());
1256
        }
1257
1258
        $fieldDescription->setAssociationAdmin($admin);
1259
    }
1260
1261
    public function getObject($id)
1262
    {
1263
        $object = $this->getModelManager()->find($this->getClass(), $id);
1264
        foreach ($this->getExtensions() as $extension) {
1265
            $extension->alterObject($this, $object);
0 ignored issues
show
Bug introduced by
It seems like $object defined by $this->getModelManager()...$this->getClass(), $id) on line 1263 can also be of type null; however, Sonata\AdminBundle\Admin...nterface::alterObject() does only seem to accept object, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1266
        }
1267
1268
        return $object;
1269
    }
1270
1271
    public function getForm()
1272
    {
1273
        $this->buildForm();
1274
1275
        return $this->form;
1276
    }
1277
1278
    public function getList()
1279
    {
1280
        $this->buildList();
1281
1282
        return $this->list;
1283
    }
1284
1285
    /**
1286
     * @final since sonata-project/admin-bundle 3.63.0
1287
     */
1288
    public function createQuery($context = 'list')
1289
    {
1290
        if (\func_num_args() > 0) {
1291
            @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...
1292
                'The $context argument of '.__METHOD__.' is deprecated since 3.3, to be removed in 4.0.',
1293
                E_USER_DEPRECATED
1294
            );
1295
        }
1296
1297
        $query = $this->getModelManager()->createQuery($this->getClass());
1298
1299
        $query = $this->configureQuery($query);
1300
        foreach ($this->extensions as $extension) {
1301
            $extension->configureQuery($this, $query, $context);
1302
        }
1303
1304
        return $query;
1305
    }
1306
1307
    public function getDatagrid()
1308
    {
1309
        $this->buildDatagrid();
1310
1311
        return $this->datagrid;
1312
    }
1313
1314
    public function buildTabMenu($action, ?AdminInterface $childAdmin = null): ItemInterface
1315
    {
1316
        if ($this->loaded['tab_menu']) {
1317
            return $this->menu;
1318
        }
1319
1320
        $this->loaded['tab_menu'] = true;
1321
1322
        $menu = $this->menuFactory->createItem('root');
1323
        $menu->setChildrenAttribute('class', 'nav navbar-nav');
1324
        $menu->setExtra('translation_domain', $this->translationDomain);
1325
1326
        // Prevents BC break with KnpMenuBundle v1.x
1327
        if (method_exists($menu, 'setCurrentUri')) {
1328
            $menu->setCurrentUri($this->getRequest()->getBaseUrl().$this->getRequest()->getPathInfo());
0 ignored issues
show
Bug introduced by
The method setCurrentUri() does not exist on Knp\Menu\ItemInterface. Did you maybe mean setCurrent()?

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...
1329
        }
1330
1331
        $this->configureTabMenu($menu, $action, $childAdmin);
1332
1333
        foreach ($this->getExtensions() as $extension) {
1334
            $extension->configureTabMenu($this, $menu, $action, $childAdmin);
1335
        }
1336
1337
        $this->menu = $menu;
1338
1339
        return $this->menu;
1340
    }
1341
1342
    /**
1343
     * @param string $action
1344
     *
1345
     * @return ItemInterface
1346
     */
1347
    public function getSideMenu($action, ?AdminInterface $childAdmin = null)
1348
    {
1349
        if ($this->isChild()) {
1350
            return $this->getParent()->getSideMenu($action, $this);
1351
        }
1352
1353
        $this->buildTabMenu($action, $childAdmin);
1354
1355
        return $this->menu;
1356
    }
1357
1358
    public function getRootCode(): string
1359
    {
1360
        return $this->getRoot()->getCode();
1361
    }
1362
1363
    public function getRoot(): AdminInterface
1364
    {
1365
        $parentFieldDescription = $this->getParentFieldDescription();
1366
1367
        if (!$parentFieldDescription) {
1368
            return $this;
1369
        }
1370
1371
        return $parentFieldDescription->getAdmin()->getRoot();
1372
    }
1373
1374
    public function setBaseControllerName($baseControllerName): void
1375
    {
1376
        $this->baseControllerName = $baseControllerName;
1377
    }
1378
1379
    public function getBaseControllerName()
1380
    {
1381
        return $this->baseControllerName;
1382
    }
1383
1384
    /**
1385
     * @param string $label
1386
     */
1387
    public function setLabel($label): void
1388
    {
1389
        $this->label = $label;
1390
    }
1391
1392
    public function getLabel()
1393
    {
1394
        return $this->label;
1395
    }
1396
1397
    /**
1398
     * @param bool $persist
1399
     *
1400
     * NEXT_MAJOR: remove this method
1401
     *
1402
     * @deprecated since sonata-project/admin-bundle 3.34, to be removed in 4.0.
1403
     */
1404
    public function setPersistFilters($persist): void
1405
    {
1406
        @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...
1407
            'The '.__METHOD__.' method is deprecated since version 3.34 and will be removed in 4.0.',
1408
            E_USER_DEPRECATED
1409
        );
1410
1411
        $this->persistFilters = $persist;
0 ignored issues
show
Deprecated Code introduced by
The property Sonata\AdminBundle\Admin...tAdmin::$persistFilters has been deprecated with message: since sonata-project/admin-bundle 3.34, to be removed in 4.0.

This property 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 property will be removed from the class and what other property to use instead.

Loading history...
1412
    }
1413
1414
    public function setFilterPersister(?FilterPersisterInterface $filterPersister = null): void
1415
    {
1416
        $this->filterPersister = $filterPersister;
1417
        // NEXT_MAJOR remove the deprecated property will be removed. Needed for persisted filter condition.
1418
        $this->persistFilters = true;
0 ignored issues
show
Deprecated Code introduced by
The property Sonata\AdminBundle\Admin...tAdmin::$persistFilters has been deprecated with message: since sonata-project/admin-bundle 3.34, to be removed in 4.0.

This property 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 property will be removed from the class and what other property to use instead.

Loading history...
1419
    }
1420
1421
    /**
1422
     * @param int $maxPerPage
1423
     */
1424
    public function setMaxPerPage($maxPerPage): void
1425
    {
1426
        $this->maxPerPage = $maxPerPage;
1427
    }
1428
1429
    /**
1430
     * @return int
1431
     */
1432
    public function getMaxPerPage()
1433
    {
1434
        return $this->maxPerPage;
1435
    }
1436
1437
    /**
1438
     * @param int $maxPageLinks
1439
     */
1440
    public function setMaxPageLinks($maxPageLinks): void
1441
    {
1442
        $this->maxPageLinks = $maxPageLinks;
1443
    }
1444
1445
    /**
1446
     * @return int
1447
     */
1448
    public function getMaxPageLinks()
1449
    {
1450
        return $this->maxPageLinks;
1451
    }
1452
1453
    public function getFormGroups()
1454
    {
1455
        if (!\is_array($this->formGroups) && 'sonata_deprecation_mute' !== (\func_get_args()[0] ?? null)) {
1456
            @trigger_error(sprintf(
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...
1457
                'Returning other type than array in method %s() is deprecated since sonata-project/admin-bundle 3.65. It will return only array in version 4.0.',
1458
                __METHOD__
1459
            ), E_USER_DEPRECATED);
1460
        }
1461
1462
        return $this->formGroups;
1463
    }
1464
1465
    public function setFormGroups(array $formGroups): void
1466
    {
1467
        $this->formGroups = $formGroups;
1468
    }
1469
1470
    public function removeFieldFromFormGroup($key): void
1471
    {
1472
        foreach ($this->formGroups as $name => $formGroup) {
0 ignored issues
show
Bug introduced by
The expression $this->formGroups of type array|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1473
            unset($this->formGroups[$name]['fields'][$key]);
1474
1475
            if (empty($this->formGroups[$name]['fields'])) {
1476
                unset($this->formGroups[$name]);
1477
            }
1478
        }
1479
    }
1480
1481
    /**
1482
     * @param string $group
1483
     */
1484
    public function reorderFormGroup($group, array $keys): void
1485
    {
1486
        // NEXT_MAJOR: Remove the argument "sonata_deprecation_mute" in the following call.
1487
        $formGroups = $this->getFormGroups('sonata_deprecation_mute');
1488
        $formGroups[$group]['fields'] = array_merge(array_flip($keys), $formGroups[$group]['fields']);
1489
        $this->setFormGroups($formGroups);
0 ignored issues
show
Bug introduced by
It seems like $formGroups defined by $this->getFormGroups('sonata_deprecation_mute') on line 1487 can also be of type boolean; however, Sonata\AdminBundle\Admin...tAdmin::setFormGroups() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1490
    }
1491
1492
    public function getFormTabs()
1493
    {
1494
        if (!\is_array($this->formTabs) && 'sonata_deprecation_mute' !== (\func_get_args()[0] ?? null)) {
1495
            @trigger_error(sprintf(
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...
1496
                'Returning other type than array in method %s() is deprecated since sonata-project/admin-bundle 3.65. It will return only array in version 4.0.',
1497
                __METHOD__
1498
            ), E_USER_DEPRECATED);
1499
        }
1500
1501
        return $this->formTabs;
1502
    }
1503
1504
    public function setFormTabs(array $formTabs): void
1505
    {
1506
        $this->formTabs = $formTabs;
1507
    }
1508
1509
    public function getShowTabs()
1510
    {
1511
        if (!\is_array($this->showTabs) && 'sonata_deprecation_mute' !== (\func_get_args()[0] ?? null)) {
1512
            @trigger_error(sprintf(
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...
1513
                'Returning other type than array in method %s() is deprecated since sonata-project/admin-bundle 3.65. It will return only array in version 4.0.',
1514
                __METHOD__
1515
            ), E_USER_DEPRECATED);
1516
        }
1517
1518
        return $this->showTabs;
1519
    }
1520
1521
    public function setShowTabs(array $showTabs): void
1522
    {
1523
        $this->showTabs = $showTabs;
1524
    }
1525
1526
    public function getShowGroups()
1527
    {
1528
        if (!\is_array($this->showGroups) && 'sonata_deprecation_mute' !== (\func_get_args()[0] ?? null)) {
1529
            @trigger_error(sprintf(
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...
1530
                'Returning other type than array in method %s() is deprecated since sonata-project/admin-bundle 3.65. It will return only array in version 4.0.',
1531
                __METHOD__
1532
            ), E_USER_DEPRECATED);
1533
        }
1534
1535
        return $this->showGroups;
1536
    }
1537
1538
    public function setShowGroups(array $showGroups): void
1539
    {
1540
        $this->showGroups = $showGroups;
1541
    }
1542
1543
    public function reorderShowGroup($group, array $keys): void
1544
    {
1545
        // NEXT_MAJOR: Remove the argument "sonata_deprecation_mute" in the following call.
1546
        $showGroups = $this->getShowGroups('sonata_deprecation_mute');
1547
        $showGroups[$group]['fields'] = array_merge(array_flip($keys), $showGroups[$group]['fields']);
1548
        $this->setShowGroups($showGroups);
0 ignored issues
show
Bug introduced by
It seems like $showGroups defined by $this->getShowGroups('sonata_deprecation_mute') on line 1546 can also be of type boolean; however, Sonata\AdminBundle\Admin...tAdmin::setShowGroups() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1549
    }
1550
1551
    public function setParentFieldDescription(FieldDescriptionInterface $parentFieldDescription): void
1552
    {
1553
        $this->parentFieldDescription = $parentFieldDescription;
1554
    }
1555
1556
    public function getParentFieldDescription()
1557
    {
1558
        return $this->parentFieldDescription;
1559
    }
1560
1561
    public function hasParentFieldDescription()
1562
    {
1563
        return $this->parentFieldDescription instanceof FieldDescriptionInterface;
1564
    }
1565
1566
    public function setSubject($subject): void
1567
    {
1568
        if (\is_object($subject) && !is_a($subject, $this->getClass(), true)) {
1569
            $message = <<<'EOT'
1570
You are trying to set entity an instance of "%s",
1571
which is not the one registered with this admin class ("%s").
1572
This is deprecated since 3.5 and will no longer be supported in 4.0.
1573
EOT;
1574
1575
            @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...
1576
                sprintf($message, \get_class($subject), $this->getClass()),
1577
                E_USER_DEPRECATED
1578
            ); // NEXT_MAJOR : throw an exception instead
1579
        }
1580
1581
        $this->subject = $subject;
1582
    }
1583
1584
    public function getSubject()
1585
    {
1586
        if (null === $this->subject && $this->request && !$this->hasParentFieldDescription()) {
1587
            $id = $this->request->get($this->getIdParameter());
1588
1589
            if (null !== $id) {
1590
                $this->subject = $this->getObject($id);
1591
            }
1592
        }
1593
1594
        return $this->subject;
1595
    }
1596
1597
    public function hasSubject()
1598
    {
1599
        return (bool) $this->getSubject();
1600
    }
1601
1602
    public function getFormFieldDescriptions()
1603
    {
1604
        $this->buildForm();
1605
1606
        return $this->formFieldDescriptions;
1607
    }
1608
1609
    public function getFormFieldDescription($name)
1610
    {
1611
        return $this->hasFormFieldDescription($name) ? $this->formFieldDescriptions[$name] : null;
1612
    }
1613
1614
    /**
1615
     * Returns true if the admin has a FieldDescription with the given $name.
1616
     *
1617
     * @param string $name
1618
     *
1619
     * @return bool
1620
     */
1621
    public function hasFormFieldDescription($name)
1622
    {
1623
        return \array_key_exists($name, $this->formFieldDescriptions) ? true : false;
1624
    }
1625
1626
    public function addFormFieldDescription($name, FieldDescriptionInterface $fieldDescription): void
1627
    {
1628
        $this->formFieldDescriptions[$name] = $fieldDescription;
1629
    }
1630
1631
    /**
1632
     * remove a FieldDescription.
1633
     *
1634
     * @param string $name
1635
     */
1636
    public function removeFormFieldDescription($name): void
1637
    {
1638
        unset($this->formFieldDescriptions[$name]);
1639
    }
1640
1641
    /**
1642
     * build and return the collection of form FieldDescription.
1643
     *
1644
     * @return array collection of form FieldDescription
1645
     */
1646
    public function getShowFieldDescriptions()
1647
    {
1648
        $this->buildShow();
1649
1650
        return $this->showFieldDescriptions;
1651
    }
1652
1653
    /**
1654
     * Returns the form FieldDescription with the given $name.
1655
     *
1656
     * @param string $name
1657
     *
1658
     * @return FieldDescriptionInterface
1659
     */
1660
    public function getShowFieldDescription($name)
1661
    {
1662
        $this->buildShow();
1663
1664
        return $this->hasShowFieldDescription($name) ? $this->showFieldDescriptions[$name] : null;
1665
    }
1666
1667
    public function hasShowFieldDescription($name)
1668
    {
1669
        return \array_key_exists($name, $this->showFieldDescriptions);
1670
    }
1671
1672
    public function addShowFieldDescription($name, FieldDescriptionInterface $fieldDescription): void
1673
    {
1674
        $this->showFieldDescriptions[$name] = $fieldDescription;
1675
    }
1676
1677
    public function removeShowFieldDescription($name): void
1678
    {
1679
        unset($this->showFieldDescriptions[$name]);
1680
    }
1681
1682
    public function getListFieldDescriptions()
1683
    {
1684
        $this->buildList();
1685
1686
        return $this->listFieldDescriptions;
1687
    }
1688
1689
    public function getListFieldDescription($name)
1690
    {
1691
        return $this->hasListFieldDescription($name) ? $this->listFieldDescriptions[$name] : null;
1692
    }
1693
1694
    public function hasListFieldDescription($name)
1695
    {
1696
        $this->buildList();
1697
1698
        return \array_key_exists($name, $this->listFieldDescriptions) ? true : false;
1699
    }
1700
1701
    public function addListFieldDescription($name, FieldDescriptionInterface $fieldDescription): void
1702
    {
1703
        $this->listFieldDescriptions[$name] = $fieldDescription;
1704
    }
1705
1706
    public function removeListFieldDescription($name): void
1707
    {
1708
        unset($this->listFieldDescriptions[$name]);
1709
    }
1710
1711
    public function getFilterFieldDescription($name)
1712
    {
1713
        return $this->hasFilterFieldDescription($name) ? $this->filterFieldDescriptions[$name] : null;
1714
    }
1715
1716
    public function hasFilterFieldDescription($name)
1717
    {
1718
        return \array_key_exists($name, $this->filterFieldDescriptions) ? true : false;
1719
    }
1720
1721
    public function addFilterFieldDescription($name, FieldDescriptionInterface $fieldDescription): void
1722
    {
1723
        $this->filterFieldDescriptions[$name] = $fieldDescription;
1724
    }
1725
1726
    public function removeFilterFieldDescription($name): void
1727
    {
1728
        unset($this->filterFieldDescriptions[$name]);
1729
    }
1730
1731
    public function getFilterFieldDescriptions()
1732
    {
1733
        $this->buildDatagrid();
1734
1735
        return $this->filterFieldDescriptions;
1736
    }
1737
1738
    public function addChild(AdminInterface $child): void
1739
    {
1740
        for ($parentAdmin = $this; null !== $parentAdmin; $parentAdmin = $parentAdmin->getParent()) {
1741
            if ($parentAdmin->getCode() !== $child->getCode()) {
1742
                continue;
1743
            }
1744
1745
            throw new \RuntimeException(sprintf(
1746
                'Circular reference detected! The child admin `%s` is already in the parent tree of the `%s` admin.',
1747
                $child->getCode(),
1748
                $this->getCode()
1749
            ));
1750
        }
1751
1752
        $this->children[$child->getCode()] = $child;
1753
1754
        $child->setParent($this);
0 ignored issues
show
Documentation introduced by
$this is of type this<Sonata\AdminBundle\Admin\AbstractAdmin>, but the function expects a object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1755
1756
        // NEXT_MAJOR: remove $args and add $field parameter to this function on next Major
1757
1758
        $args = \func_get_args();
1759
1760
        if (isset($args[1])) {
1761
            $child->addParentAssociationMapping($this->getCode(), $args[1]);
1762
        } else {
1763
            @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...
1764
                'Calling "addChild" without second argument is deprecated since'
1765
                .' sonata-project/admin-bundle 3.35 and will not be allowed in 4.0.',
1766
                E_USER_DEPRECATED
1767
            );
1768
        }
1769
    }
1770
1771
    public function hasChild($code)
1772
    {
1773
        return isset($this->children[$code]);
1774
    }
1775
1776
    public function getChildren()
1777
    {
1778
        return $this->children;
1779
    }
1780
1781
    public function getChild($code)
1782
    {
1783
        return $this->hasChild($code) ? $this->children[$code] : null;
1784
    }
1785
1786
    public function setParent(AdminInterface $parent): void
1787
    {
1788
        $this->parent = $parent;
1789
    }
1790
1791
    public function getParent()
1792
    {
1793
        return $this->parent;
1794
    }
1795
1796
    final public function getRootAncestor()
1797
    {
1798
        $parent = $this;
1799
1800
        while ($parent->isChild()) {
1801
            $parent = $parent->getParent();
1802
        }
1803
1804
        return $parent;
1805
    }
1806
1807
    final public function getChildDepth()
1808
    {
1809
        $parent = $this;
1810
        $depth = 0;
1811
1812
        while ($parent->isChild()) {
1813
            $parent = $parent->getParent();
1814
            ++$depth;
1815
        }
1816
1817
        return $depth;
1818
    }
1819
1820
    final public function getCurrentLeafChildAdmin()
1821
    {
1822
        $child = $this->getCurrentChildAdmin();
1823
1824
        if (null === $child) {
1825
            return null;
1826
        }
1827
1828
        for ($c = $child; null !== $c; $c = $child->getCurrentChildAdmin()) {
1829
            $child = $c;
1830
        }
1831
1832
        return $child;
1833
    }
1834
1835
    public function isChild()
1836
    {
1837
        return $this->parent instanceof AdminInterface;
1838
    }
1839
1840
    /**
1841
     * Returns true if the admin has children, false otherwise.
1842
     *
1843
     * @return bool if the admin has children
1844
     */
1845
    public function hasChildren()
1846
    {
1847
        return \count($this->children) > 0;
1848
    }
1849
1850
    public function setUniqid($uniqid): void
1851
    {
1852
        $this->uniqid = $uniqid;
1853
    }
1854
1855
    public function getUniqid()
1856
    {
1857
        if (!$this->uniqid) {
1858
            $this->uniqid = 's'.uniqid();
1859
        }
1860
1861
        return $this->uniqid;
1862
    }
1863
1864
    /**
1865
     * {@inheritdoc}
1866
     */
1867
    public function getClassnameLabel()
1868
    {
1869
        return $this->classnameLabel;
1870
    }
1871
1872
    public function getPersistentParameters()
1873
    {
1874
        $parameters = [];
1875
1876
        foreach ($this->getExtensions() as $extension) {
1877
            $params = $extension->getPersistentParameters($this);
1878
1879
            if (!\is_array($params)) {
1880
                throw new \RuntimeException(sprintf('The %s::getPersistentParameters must return an array', \get_class($extension)));
1881
            }
1882
1883
            $parameters = array_merge($parameters, $params);
1884
        }
1885
1886
        return $parameters;
1887
    }
1888
1889
    /**
1890
     * {@inheritdoc}
1891
     */
1892
    public function getPersistentParameter(string $name)
1893
    {
1894
        $parameters = $this->getPersistentParameters();
1895
1896
        return $parameters[$name] ?? null;
1897
    }
1898
1899
    public function setCurrentChild($currentChild): void
1900
    {
1901
        $this->currentChild = $currentChild;
1902
    }
1903
1904
    /**
1905
     * NEXT_MAJOR: Remove this method.
1906
     *
1907
     * @deprecated since sonata-project/admin-bundle 3.65, to be removed in 4.0
1908
     */
1909
    public function getCurrentChild()
1910
    {
1911
        @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...
1912
            sprintf(
1913
                'The %s() method is deprecated since version 3.65 and will be removed in 4.0. Use %s::isCurrentChild() instead.',
1914
                __METHOD__,
1915
                __CLASS__
1916
            ),
1917
            E_USER_DEPRECATED
1918
        );
1919
1920
        return $this->currentChild;
1921
    }
1922
1923
    public function isCurrentChild(): bool
1924
    {
1925
        return $this->currentChild;
1926
    }
1927
1928
    /**
1929
     * Returns the current child admin instance.
1930
     *
1931
     * @return AdminInterface|null the current child admin instance
1932
     */
1933
    public function getCurrentChildAdmin()
1934
    {
1935
        foreach ($this->children as $children) {
1936
            if ($children->isCurrentChild()) {
1937
                return $children;
1938
            }
1939
        }
1940
1941
        return null;
1942
    }
1943
1944
    public function trans($id, array $parameters = [], $domain = null, $locale = null)
1945
    {
1946
        @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...
1947
            'The '.__METHOD__.' method is deprecated since version 3.9 and will be removed in 4.0.',
1948
            E_USER_DEPRECATED
1949
        );
1950
1951
        $domain = $domain ?: $this->getTranslationDomain();
1952
1953
        return $this->translator->trans($id, $parameters, $domain, $locale);
0 ignored issues
show
Deprecated Code introduced by
The property Sonata\AdminBundle\Admin...tractAdmin::$translator has been deprecated with message: since sonata-project/admin-bundle 3.9, to be removed with 4.0

This property 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 property will be removed from the class and what other property to use instead.

Loading history...
1954
    }
1955
1956
    /**
1957
     * Translate a message id.
1958
     *
1959
     * NEXT_MAJOR: remove this method
1960
     *
1961
     * @param string      $id
1962
     * @param int         $count
1963
     * @param string|null $domain
1964
     * @param string|null $locale
1965
     *
1966
     * @return string the translated string
1967
     *
1968
     * @deprecated since sonata-project/admin-bundle 3.9, to be removed with 4.0
1969
     */
1970
    public function transChoice($id, $count, array $parameters = [], $domain = null, $locale = null)
1971
    {
1972
        @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...
1973
            'The '.__METHOD__.' method is deprecated since version 3.9 and will be removed in 4.0.',
1974
            E_USER_DEPRECATED
1975
        );
1976
1977
        $domain = $domain ?: $this->getTranslationDomain();
1978
1979
        return $this->translator->transChoice($id, $count, $parameters, $domain, $locale);
0 ignored issues
show
Deprecated Code introduced by
The property Sonata\AdminBundle\Admin...tractAdmin::$translator has been deprecated with message: since sonata-project/admin-bundle 3.9, to be removed with 4.0

This property 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 property will be removed from the class and what other property to use instead.

Loading history...
1980
    }
1981
1982
    public function setTranslationDomain($translationDomain): void
1983
    {
1984
        $this->translationDomain = $translationDomain;
1985
    }
1986
1987
    public function getTranslationDomain()
1988
    {
1989
        return $this->translationDomain;
1990
    }
1991
1992
    /**
1993
     * {@inheritdoc}
1994
     *
1995
     * NEXT_MAJOR: remove this method
1996
     *
1997
     * @deprecated since sonata-project/admin-bundle 3.9, to be removed with 4.0
1998
     */
1999
    public function setTranslator(TranslatorInterface $translator): void
2000
    {
2001
        $args = \func_get_args();
2002
        if (isset($args[1]) && $args[1]) {
2003
            @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...
2004
                'The '.__METHOD__.' method is deprecated since version 3.9 and will be removed in 4.0.',
2005
                E_USER_DEPRECATED
2006
            );
2007
        }
2008
2009
        $this->translator = $translator;
0 ignored issues
show
Deprecated Code introduced by
The property Sonata\AdminBundle\Admin...tractAdmin::$translator has been deprecated with message: since sonata-project/admin-bundle 3.9, to be removed with 4.0

This property 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 property will be removed from the class and what other property to use instead.

Loading history...
2010
    }
2011
2012
    /**
2013
     * {@inheritdoc}
2014
     *
2015
     * NEXT_MAJOR: remove this method
2016
     *
2017
     * @deprecated since sonata-project/admin-bundle 3.9, to be removed with 4.0
2018
     */
2019
    public function getTranslator()
2020
    {
2021
        @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...
2022
            'The '.__METHOD__.' method is deprecated since version 3.9 and will be removed in 4.0.',
2023
            E_USER_DEPRECATED
2024
        );
2025
2026
        return $this->translator;
0 ignored issues
show
Deprecated Code introduced by
The property Sonata\AdminBundle\Admin...tractAdmin::$translator has been deprecated with message: since sonata-project/admin-bundle 3.9, to be removed with 4.0

This property 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 property will be removed from the class and what other property to use instead.

Loading history...
2027
    }
2028
2029
    public function getTranslationLabel($label, $context = '', $type = '')
2030
    {
2031
        return $this->getLabelTranslatorStrategy()->getLabel($label, $context, $type);
2032
    }
2033
2034
    public function setRequest(Request $request): void
0 ignored issues
show
Bug introduced by
You have injected the Request via parameter $request. This is generally not recommended as there might be multiple instances during a request cycle (f.e. when using sub-requests). Instead, it is recommended to inject the RequestStack and retrieve the current request each time you need it via getCurrentRequest().
Loading history...
2035
    {
2036
        $this->request = $request;
2037
2038
        foreach ($this->getChildren() as $children) {
2039
            $children->setRequest($request);
2040
        }
2041
    }
2042
2043
    public function getRequest()
2044
    {
2045
        if (!$this->request) {
2046
            throw new \RuntimeException('The Request object has not been set');
2047
        }
2048
2049
        return $this->request;
2050
    }
2051
2052
    public function hasRequest()
2053
    {
2054
        return null !== $this->request;
2055
    }
2056
2057
    public function setFormContractor(FormContractorInterface $formBuilder): void
2058
    {
2059
        $this->formContractor = $formBuilder;
2060
    }
2061
2062
    /**
2063
     * @return FormContractorInterface
2064
     */
2065
    public function getFormContractor()
2066
    {
2067
        return $this->formContractor;
2068
    }
2069
2070
    public function setDatagridBuilder(DatagridBuilderInterface $datagridBuilder): void
2071
    {
2072
        $this->datagridBuilder = $datagridBuilder;
2073
    }
2074
2075
    public function getDatagridBuilder()
2076
    {
2077
        return $this->datagridBuilder;
2078
    }
2079
2080
    public function setListBuilder(ListBuilderInterface $listBuilder): void
2081
    {
2082
        $this->listBuilder = $listBuilder;
2083
    }
2084
2085
    public function getListBuilder()
2086
    {
2087
        return $this->listBuilder;
2088
    }
2089
2090
    public function setShowBuilder(ShowBuilderInterface $showBuilder): void
2091
    {
2092
        $this->showBuilder = $showBuilder;
2093
    }
2094
2095
    /**
2096
     * @return ShowBuilderInterface
2097
     */
2098
    public function getShowBuilder()
2099
    {
2100
        return $this->showBuilder;
2101
    }
2102
2103
    public function setConfigurationPool(Pool $configurationPool): void
2104
    {
2105
        $this->configurationPool = $configurationPool;
2106
    }
2107
2108
    /**
2109
     * @return Pool
2110
     */
2111
    public function getConfigurationPool()
2112
    {
2113
        return $this->configurationPool;
2114
    }
2115
2116
    public function setRouteGenerator(RouteGeneratorInterface $routeGenerator): void
2117
    {
2118
        $this->routeGenerator = $routeGenerator;
2119
    }
2120
2121
    /**
2122
     * @return RouteGeneratorInterface
2123
     */
2124
    public function getRouteGenerator()
2125
    {
2126
        return $this->routeGenerator;
2127
    }
2128
2129
    public function getCode()
2130
    {
2131
        return $this->code;
2132
    }
2133
2134
    public function getBaseCodeRoute()
2135
    {
2136
        if ($this->isChild()) {
2137
            return $this->getParent()->getBaseCodeRoute().'|'.$this->getCode();
2138
        }
2139
2140
        return $this->getCode();
2141
    }
2142
2143
    public function getModelManager()
2144
    {
2145
        return $this->modelManager;
2146
    }
2147
2148
    public function setModelManager(ModelManagerInterface $modelManager): void
2149
    {
2150
        $this->modelManager = $modelManager;
2151
    }
2152
2153
    public function getManagerType()
2154
    {
2155
        return $this->managerType;
2156
    }
2157
2158
    /**
2159
     * @param string $type
2160
     */
2161
    public function setManagerType($type): void
2162
    {
2163
        $this->managerType = $type;
2164
    }
2165
2166
    public function getObjectIdentifier()
2167
    {
2168
        return $this->getCode();
2169
    }
2170
2171
    /**
2172
     * Set the roles and permissions per role.
2173
     */
2174
    public function setSecurityInformation(array $information): void
2175
    {
2176
        $this->securityInformation = $information;
2177
    }
2178
2179
    public function getSecurityInformation()
2180
    {
2181
        return $this->securityInformation;
2182
    }
2183
2184
    /**
2185
     * Return the list of permissions the user should have in order to display the admin.
2186
     *
2187
     * @param string $context
2188
     *
2189
     * @return array
2190
     */
2191
    public function getPermissionsShow($context)
2192
    {
2193
        switch ($context) {
2194
            case self::CONTEXT_DASHBOARD:
2195
            case self::CONTEXT_MENU:
2196
            default:
2197
                return ['LIST'];
2198
        }
2199
    }
2200
2201
    public function showIn($context)
2202
    {
2203
        switch ($context) {
2204
            case self::CONTEXT_DASHBOARD:
2205
            case self::CONTEXT_MENU:
2206
            default:
2207
                return $this->isGranted($this->getPermissionsShow($context));
0 ignored issues
show
Documentation introduced by
$this->getPermissionsShow($context) is of type array<integer,string,{"0":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2208
        }
2209
    }
2210
2211
    public function createObjectSecurity($object): void
2212
    {
2213
        $this->getSecurityHandler()->createObjectSecurity($this, $object);
2214
    }
2215
2216
    public function setSecurityHandler(SecurityHandlerInterface $securityHandler): void
2217
    {
2218
        $this->securityHandler = $securityHandler;
2219
    }
2220
2221
    public function getSecurityHandler()
2222
    {
2223
        return $this->securityHandler;
2224
    }
2225
2226
    public function isGranted($name, $object = null)
2227
    {
2228
        $objectRef = $object ? '/'.spl_object_hash($object).'#'.$this->id($object) : '';
2229
        $key = md5(json_encode($name).$objectRef);
2230
2231
        if (!\array_key_exists($key, $this->cacheIsGranted)) {
2232
            $this->cacheIsGranted[$key] = $this->securityHandler->isGranted($this, $name, $object ?: $this);
2233
        }
2234
2235
        return $this->cacheIsGranted[$key];
2236
    }
2237
2238
    public function getUrlSafeIdentifier($entity)
2239
    {
2240
        return $this->getModelManager()->getUrlSafeIdentifier($entity);
2241
    }
2242
2243
    public function getNormalizedIdentifier($entity)
2244
    {
2245
        return $this->getModelManager()->getNormalizedIdentifier($entity);
2246
    }
2247
2248
    public function id($entity)
2249
    {
2250
        return $this->getNormalizedIdentifier($entity);
2251
    }
2252
2253
    public function setValidator(ValidatorInterface $validator): void
2254
    {
2255
        $this->validator = $validator;
2256
    }
2257
2258
    public function getValidator()
2259
    {
2260
        return $this->validator;
2261
    }
2262
2263
    public function getShow()
2264
    {
2265
        $this->buildShow();
2266
2267
        return $this->show;
2268
    }
2269
2270
    public function setFormTheme(array $formTheme): void
2271
    {
2272
        $this->formTheme = $formTheme;
2273
    }
2274
2275
    public function getFormTheme()
2276
    {
2277
        return $this->formTheme;
2278
    }
2279
2280
    public function setFilterTheme(array $filterTheme): void
2281
    {
2282
        $this->filterTheme = $filterTheme;
2283
    }
2284
2285
    public function getFilterTheme()
2286
    {
2287
        return $this->filterTheme;
2288
    }
2289
2290
    public function addExtension(AdminExtensionInterface $extension): void
2291
    {
2292
        $this->extensions[] = $extension;
2293
    }
2294
2295
    public function getExtensions()
2296
    {
2297
        return $this->extensions;
2298
    }
2299
2300
    public function setMenuFactory(FactoryInterface $menuFactory): void
2301
    {
2302
        $this->menuFactory = $menuFactory;
2303
    }
2304
2305
    public function getMenuFactory()
2306
    {
2307
        return $this->menuFactory;
2308
    }
2309
2310
    public function setRouteBuilder(RouteBuilderInterface $routeBuilder): void
2311
    {
2312
        $this->routeBuilder = $routeBuilder;
2313
    }
2314
2315
    public function getRouteBuilder()
2316
    {
2317
        return $this->routeBuilder;
2318
    }
2319
2320
    public function toString($object)
2321
    {
2322
        if (!\is_object($object)) {
2323
            return '';
2324
        }
2325
2326
        if (method_exists($object, '__toString') && null !== $object->__toString()) {
2327
            return (string) $object;
2328
        }
2329
2330
        return sprintf('%s:%s', ClassUtils::getClass($object), spl_object_hash($object));
2331
    }
2332
2333
    public function setLabelTranslatorStrategy(LabelTranslatorStrategyInterface $labelTranslatorStrategy): void
2334
    {
2335
        $this->labelTranslatorStrategy = $labelTranslatorStrategy;
2336
    }
2337
2338
    public function getLabelTranslatorStrategy()
2339
    {
2340
        return $this->labelTranslatorStrategy;
2341
    }
2342
2343
    public function supportsPreviewMode()
2344
    {
2345
        return $this->supportsPreviewMode;
2346
    }
2347
2348
    /**
2349
     * Set custom per page options.
2350
     */
2351
    public function setPerPageOptions(array $options): void
2352
    {
2353
        $this->perPageOptions = $options;
2354
    }
2355
2356
    /**
2357
     * Returns predefined per page options.
2358
     *
2359
     * @return array
2360
     */
2361
    public function getPerPageOptions()
2362
    {
2363
        return $this->perPageOptions;
2364
    }
2365
2366
    /**
2367
     * Set pager type.
2368
     *
2369
     * @param string $pagerType
2370
     */
2371
    public function setPagerType($pagerType): void
2372
    {
2373
        $this->pagerType = $pagerType;
2374
    }
2375
2376
    /**
2377
     * Get pager type.
2378
     *
2379
     * @return string
2380
     */
2381
    public function getPagerType()
2382
    {
2383
        return $this->pagerType;
2384
    }
2385
2386
    /**
2387
     * Returns true if the per page value is allowed, false otherwise.
2388
     *
2389
     * @param int $perPage
2390
     *
2391
     * @return bool
2392
     */
2393
    public function determinedPerPageValue($perPage)
2394
    {
2395
        return \in_array($perPage, $this->perPageOptions, true);
2396
    }
2397
2398
    public function isAclEnabled()
2399
    {
2400
        return $this->getSecurityHandler() instanceof AclSecurityHandlerInterface;
2401
    }
2402
2403
    public function getObjectMetadata($object)
2404
    {
2405
        return new Metadata($this->toString($object));
2406
    }
2407
2408
    public function getListModes()
2409
    {
2410
        return $this->listModes;
2411
    }
2412
2413
    public function setListMode($mode): void
2414
    {
2415
        if (!$this->hasRequest()) {
2416
            throw new \RuntimeException(sprintf('No request attached to the current admin: %s', $this->getCode()));
2417
        }
2418
2419
        $this->getRequest()->getSession()->set(sprintf('%s.list_mode', $this->getCode()), $mode);
2420
    }
2421
2422
    public function getListMode()
2423
    {
2424
        if (!$this->hasRequest()) {
2425
            return 'list';
2426
        }
2427
2428
        return $this->getRequest()->getSession()->get(sprintf('%s.list_mode', $this->getCode()), 'list');
2429
    }
2430
2431
    public function getAccessMapping()
2432
    {
2433
        return $this->accessMapping;
2434
    }
2435
2436
    public function checkAccess($action, $object = null): void
2437
    {
2438
        $access = $this->getAccess();
2439
2440
        if (!\array_key_exists($action, $access)) {
2441
            throw new \InvalidArgumentException(sprintf(
2442
                'Action "%s" could not be found in access mapping.'
2443
                .' Please make sure your action is defined into your admin class accessMapping property.',
2444
                $action
2445
            ));
2446
        }
2447
2448
        if (!\is_array($access[$action])) {
2449
            $access[$action] = [$access[$action]];
2450
        }
2451
2452
        foreach ($access[$action] as $role) {
2453
            if (false === $this->isGranted($role, $object)) {
2454
                throw new AccessDeniedException(sprintf('Access Denied to the action %s and role %s', $action, $role));
2455
            }
2456
        }
2457
    }
2458
2459
    /**
2460
     * {@inheritdoc}
2461
     */
2462
    public function hasAccess(string $action, ?object $object = null): bool
2463
    {
2464
        $access = $this->getAccess();
2465
2466
        if (!\array_key_exists($action, $access)) {
2467
            return false;
2468
        }
2469
2470
        if (!\is_array($access[$action])) {
2471
            $access[$action] = [$access[$action]];
2472
        }
2473
2474
        foreach ($access[$action] as $role) {
2475
            if (false === $this->isGranted($role, $object)) {
2476
                return false;
2477
            }
2478
        }
2479
2480
        return true;
2481
    }
2482
2483
    /**
2484
     * @param object|null $object
2485
     */
2486
    final public function getActionButtons(string $action, $object = null): array
2487
    {
2488
        $buttonList = [];
2489
2490
        if (\in_array($action, ['tree', 'show', 'edit', 'delete', 'list', 'batch'], true)
2491
            && $this->hasAccess('create')
2492
            && $this->hasRoute('create')
2493
        ) {
2494
            $buttonList['create'] = [
2495
                'template' => $this->getTemplateRegistry()->getTemplate('button_create'),
2496
            ];
2497
        }
2498
2499
        if (\in_array($action, ['show', 'delete', 'acl', 'history'], true)
2500
            && $this->canAccessObject('edit', $object)
0 ignored issues
show
Documentation introduced by
$object is of type object|null, but the function expects a object<Sonata\AdminBundle\Admin\object>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2501
            && $this->hasRoute('edit')
2502
        ) {
2503
            $buttonList['edit'] = [
2504
                'template' => $this->getTemplateRegistry()->getTemplate('button_edit'),
2505
            ];
2506
        }
2507
2508
        if (\in_array($action, ['show', 'edit', 'acl'], true)
2509
            && $this->canAccessObject('history', $object)
0 ignored issues
show
Documentation introduced by
$object is of type object|null, but the function expects a object<Sonata\AdminBundle\Admin\object>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2510
            && $this->hasRoute('history')
2511
        ) {
2512
            $buttonList['history'] = [
2513
                'template' => $this->getTemplateRegistry()->getTemplate('button_history'),
2514
            ];
2515
        }
2516
2517
        if (\in_array($action, ['edit', 'history'], true)
2518
            && $this->isAclEnabled()
2519
            && $this->canAccessObject('acl', $object)
0 ignored issues
show
Documentation introduced by
$object is of type object|null, but the function expects a object<Sonata\AdminBundle\Admin\object>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2520
            && $this->hasRoute('acl')
2521
        ) {
2522
            $buttonList['acl'] = [
2523
                'template' => $this->getTemplateRegistry()->getTemplate('button_acl'),
2524
            ];
2525
        }
2526
2527
        if (\in_array($action, ['edit', 'history', 'acl'], true)
2528
            && $this->canAccessObject('show', $object)
0 ignored issues
show
Documentation introduced by
$object is of type object|null, but the function expects a object<Sonata\AdminBundle\Admin\object>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2529
            && \count($this->getShow()) > 0
2530
            && $this->hasRoute('show')
2531
        ) {
2532
            $buttonList['show'] = [
2533
                'template' => $this->getTemplateRegistry()->getTemplate('button_show'),
2534
            ];
2535
        }
2536
2537
        if (\in_array($action, ['show', 'edit', 'delete', 'acl', 'batch'], true)
2538
            && $this->hasAccess('list')
2539
            && $this->hasRoute('list')
2540
        ) {
2541
            $buttonList['list'] = [
2542
                'template' => $this->getTemplateRegistry()->getTemplate('button_list'),
2543
            ];
2544
        }
2545
2546
        $buttonList = $this->configureActionButtons($buttonList, $action, $object);
2547
2548
        foreach ($this->getExtensions() as $extension) {
2549
            $buttonList = $extension->configureActionButtons($this, $buttonList, $action, $object);
0 ignored issues
show
Bug introduced by
It seems like $object defined by parameter $object on line 2486 can also be of type null; however, Sonata\AdminBundle\Admin...onfigureActionButtons() does only seem to accept object, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
2550
        }
2551
2552
        return $buttonList;
2553
    }
2554
2555
    /**
2556
     * {@inheritdoc}
2557
     */
2558
    public function getDashboardActions()
2559
    {
2560
        $actions = [];
2561
2562
        if ($this->hasRoute('create') && $this->hasAccess('create')) {
2563
            $actions['create'] = [
2564
                'label' => 'link_add',
2565
                'translation_domain' => 'SonataAdminBundle',
2566
                'template' => $this->getTemplateRegistry()->getTemplate('action_create'),
2567
                'url' => $this->generateUrl('create'),
2568
                'icon' => 'plus-circle',
2569
            ];
2570
        }
2571
2572
        if ($this->hasRoute('list') && $this->hasAccess('list')) {
2573
            $actions['list'] = [
2574
                'label' => 'link_list',
2575
                'translation_domain' => 'SonataAdminBundle',
2576
                'url' => $this->generateUrl('list'),
2577
                'icon' => 'list',
2578
            ];
2579
        }
2580
2581
        return $actions;
2582
    }
2583
2584
    /**
2585
     * {@inheritdoc}
2586
     */
2587
    final public function showMosaicButton($isShown): void
2588
    {
2589
        if ($isShown) {
2590
            $this->listModes['mosaic'] = ['class' => static::MOSAIC_ICON_CLASS];
2591
        } else {
2592
            unset($this->listModes['mosaic']);
2593
        }
2594
    }
2595
2596
    /**
2597
     * @param object $object
2598
     */
2599
    final public function getSearchResultLink($object): ?string
2600
    {
2601
        foreach ($this->searchResultActions as $action) {
2602
            if ($this->hasRoute($action) && $this->hasAccess($action, $object)) {
2603
                return $this->generateObjectUrl($action, $object);
2604
            }
2605
        }
2606
2607
        return null;
2608
    }
2609
2610
    /**
2611
     * Checks if a filter type is set to a default value.
2612
     */
2613
    final public function isDefaultFilter(string $name): bool
2614
    {
2615
        $filter = $this->getFilterParameters();
2616
        $default = $this->getDefaultFilterValues();
2617
2618
        if (!\array_key_exists($name, $filter) || !\array_key_exists($name, $default)) {
2619
            return false;
2620
        }
2621
2622
        return $filter[$name] === $default[$name];
2623
    }
2624
2625
    public function canAccessObject(string $action, object $object): bool
2626
    {
2627
        return $object && $this->id($object) && $this->hasAccess($action, $object);
2628
    }
2629
2630
    public function configureActionButtons(array $buttonList, string $action, ?object $object = null): array
2631
    {
2632
        return $buttonList;
2633
    }
2634
2635
    /**
2636
     * Hook to run after initilization.
2637
     */
2638
    protected function configure(): void
2639
    {
2640
    }
2641
2642
    protected function configureQuery(ProxyQueryInterface $query): ProxyQueryInterface
2643
    {
2644
        return $query;
2645
    }
2646
2647
    /**
2648
     * urlize the given word.
2649
     *
2650
     * @param string $word
2651
     * @param string $sep  the separator
2652
     *
2653
     * @return string
2654
     */
2655
    final protected function urlize($word, $sep = '_')
2656
    {
2657
        return strtolower(preg_replace('/[^a-z0-9_]/i', $sep.'$1', $word));
2658
    }
2659
2660
    final protected function getTemplateRegistry(): MutableTemplateRegistryInterface
2661
    {
2662
        return $this->templateRegistry;
2663
    }
2664
2665
    /**
2666
     * Returns a list of default filters.
2667
     *
2668
     * @return array
2669
     */
2670
    final protected function getDefaultFilterValues()
2671
    {
2672
        $defaultFilterValues = [];
2673
2674
        $this->configureDefaultFilterValues($defaultFilterValues);
2675
2676
        foreach ($this->getExtensions() as $extension) {
2677
            // NEXT_MAJOR: remove method check
2678
            if (method_exists($extension, 'configureDefaultFilterValues')) {
2679
                $extension->configureDefaultFilterValues($this, $defaultFilterValues);
2680
            }
2681
        }
2682
2683
        return $defaultFilterValues;
2684
    }
2685
2686
    protected function configureFormFields(FormMapper $form): void
2687
    {
2688
    }
2689
2690
    protected function configureListFields(ListMapper $list): void
2691
    {
2692
    }
2693
2694
    protected function configureDatagridFilters(DatagridMapper $filter): void
2695
    {
2696
    }
2697
2698
    protected function configureShowFields(ShowMapper $show): void
2699
    {
2700
    }
2701
2702
    protected function configureRoutes(RouteCollection $collection): void
2703
    {
2704
    }
2705
2706
    /**
2707
     * Allows you to customize batch actions.
2708
     *
2709
     * @param array $actions List of actions
2710
     *
2711
     * @return array
2712
     */
2713
    protected function configureBatchActions($actions)
2714
    {
2715
        return $actions;
2716
    }
2717
2718
    /**
2719
     * NEXT_MAJOR: remove this method.
2720
     *
2721
     * @deprecated Use configureTabMenu instead
2722
     */
2723
    protected function configureSideMenu(ItemInterface $menu, $action, ?AdminInterface $childAdmin = null)
0 ignored issues
show
Unused Code introduced by
The parameter $menu 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...
Unused Code introduced by
The parameter $action 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...
Unused Code introduced by
The parameter $childAdmin 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...
2724
    {
2725
    }
2726
2727
    /**
2728
     * Configures the tab menu in your admin.
2729
     *
2730
     * @param string $action
2731
     */
2732
    protected function configureTabMenu(ItemInterface $menu, $action, ?AdminInterface $childAdmin = null)
2733
    {
2734
        // Use configureSideMenu not to mess with previous overrides
2735
        // NEXT_MAJOR: remove this line
2736
        $this->configureSideMenu($menu, $action, $childAdmin);
0 ignored issues
show
Deprecated Code introduced by
The method Sonata\AdminBundle\Admin...in::configureSideMenu() has been deprecated with message: Use configureTabMenu 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...
2737
    }
2738
2739
    /**
2740
     * build the view FieldDescription array.
2741
     */
2742
    protected function buildShow(): void
2743
    {
2744
        if ($this->show) {
2745
            return;
2746
        }
2747
2748
        $this->show = new FieldDescriptionCollection();
2749
        $mapper = new ShowMapper($this->showBuilder, $this->show, $this);
2750
2751
        $this->configureShowFields($mapper);
2752
2753
        foreach ($this->getExtensions() as $extension) {
2754
            $extension->configureShowFields($mapper);
2755
        }
2756
    }
2757
2758
    /**
2759
     * build the list FieldDescription array.
2760
     */
2761
    protected function buildList(): void
2762
    {
2763
        if ($this->list) {
2764
            return;
2765
        }
2766
2767
        $this->list = $this->getListBuilder()->getBaseList();
2768
2769
        $mapper = new ListMapper($this->getListBuilder(), $this->list, $this);
2770
2771
        if (\count($this->getBatchActions()) > 0 && $this->hasRequest() && !$this->getRequest()->isXmlHttpRequest()) {
2772
            $fieldDescription = $this->getModelManager()->getNewFieldDescriptionInstance(
2773
                $this->getClass(),
2774
                'batch',
2775
                [
2776
                    'label' => 'batch',
2777
                    'code' => '_batch',
2778
                    'sortable' => false,
2779
                    'virtual_field' => true,
2780
                ]
2781
            );
2782
2783
            $fieldDescription->setAdmin($this);
2784
            $fieldDescription->setTemplate($this->getTemplateRegistry()->getTemplate('batch'));
2785
2786
            $mapper->add($fieldDescription, 'batch');
2787
        }
2788
2789
        $this->configureListFields($mapper);
2790
2791
        foreach ($this->getExtensions() as $extension) {
2792
            $extension->configureListFields($mapper);
2793
        }
2794
2795
        if ($this->hasRequest() && $this->getRequest()->isXmlHttpRequest()) {
2796
            $fieldDescription = $this->getModelManager()->getNewFieldDescriptionInstance(
2797
                $this->getClass(),
2798
                'select',
2799
                [
2800
                    'label' => false,
2801
                    'code' => '_select',
2802
                    'sortable' => false,
2803
                    'virtual_field' => false,
2804
                ]
2805
            );
2806
2807
            $fieldDescription->setAdmin($this);
2808
            $fieldDescription->setTemplate($this->getTemplateRegistry()->getTemplate('select'));
2809
2810
            $mapper->add($fieldDescription, 'select');
2811
        }
2812
    }
2813
2814
    /**
2815
     * Build the form FieldDescription collection.
2816
     */
2817
    protected function buildForm(): void
2818
    {
2819
        if ($this->form) {
2820
            return;
2821
        }
2822
2823
        // append parent object if any
2824
        // todo : clean the way the Admin class can retrieve set the object
2825
        if ($this->isChild() && $this->getParentAssociationMapping()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getParentAssociationMapping() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null 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...
2826
            $parent = $this->getParent()->getObject($this->request->get($this->getParent()->getIdParameter()));
2827
2828
            $propertyAccessor = $this->getConfigurationPool()->getPropertyAccessor();
2829
            $propertyPath = new PropertyPath($this->getParentAssociationMapping());
2830
2831
            $object = $this->getSubject();
2832
2833
            $value = $propertyAccessor->getValue($object, $propertyPath);
0 ignored issues
show
Bug introduced by
It seems like $object defined by $this->getSubject() on line 2831 can also be of type null; however, Symfony\Component\Proper...orInterface::getValue() does only seem to accept object|array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
2834
2835
            if (\is_array($value) || $value instanceof \ArrayAccess) {
2836
                $value[] = $parent;
2837
                $propertyAccessor->setValue($object, $propertyPath, $value);
0 ignored issues
show
Bug introduced by
It seems like $object defined by $this->getSubject() on line 2831 can also be of type null; however, Symfony\Component\Proper...orInterface::setValue() does only seem to accept object|array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
2838
            } else {
2839
                $propertyAccessor->setValue($object, $propertyPath, $parent);
0 ignored issues
show
Bug introduced by
It seems like $object defined by $this->getSubject() on line 2831 can also be of type null; however, Symfony\Component\Proper...orInterface::setValue() does only seem to accept object|array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
2840
            }
2841
        }
2842
2843
        $formBuilder = $this->getFormBuilder();
2844
        $formBuilder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event): void {
2845
            $this->preValidate($event->getData());
2846
        }, 100);
2847
2848
        $this->form = $formBuilder->getForm();
0 ignored issues
show
Documentation Bug introduced by
It seems like $formBuilder->getForm() of type object<Symfony\Component\Form\FormInterface> is incompatible with the declared type object<Symfony\Component\Form\Form>|null of property $form.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
2849
    }
2850
2851
    /**
2852
     * Gets the subclass corresponding to the given name.
2853
     *
2854
     * @param string $name The name of the sub class
2855
     *
2856
     * @return string the subclass
2857
     */
2858
    protected function getSubClass($name)
2859
    {
2860
        if ($this->hasSubClass($name)) {
2861
            return $this->subClasses[$name];
2862
        }
2863
2864
        throw new \RuntimeException(sprintf(
2865
            'Unable to find the subclass `%s` for admin `%s`',
2866
            $name,
2867
            static::class
2868
        ));
2869
    }
2870
2871
    /**
2872
     * Attach the inline validator to the model metadata, this must be done once per admin.
2873
     */
2874
    protected function attachInlineValidator(): void
2875
    {
2876
        $admin = $this;
2877
2878
        // add the custom inline validation option
2879
        $metadata = $this->validator->getMetadataFor($this->getClass());
2880
2881
        $metadata->addConstraint(new InlineConstraint([
2882
            'service' => $this,
2883
            'method' => static function (ErrorElement $errorElement, $object) use ($admin): void {
2884
                /* @var \Sonata\AdminBundle\Admin\AdminInterface $admin */
2885
2886
                // This avoid the main validation to be cascaded to children
2887
                // The problem occurs when a model Page has a collection of Page as property
2888
                if ($admin->hasSubject() && spl_object_hash($object) !== spl_object_hash($admin->getSubject())) {
2889
                    return;
2890
                }
2891
2892
                $admin->validate($errorElement, $object);
2893
2894
                foreach ($admin->getExtensions() as $extension) {
2895
                    $extension->validate($admin, $errorElement, $object);
2896
                }
2897
            },
2898
            'serializingWarning' => true,
2899
        ]));
2900
    }
2901
2902
    /**
2903
     * Predefine per page options.
2904
     */
2905
    protected function predefinePerPageOptions(): void
2906
    {
2907
        array_unshift($this->perPageOptions, $this->maxPerPage);
2908
        $this->perPageOptions = array_unique($this->perPageOptions);
2909
        sort($this->perPageOptions);
2910
    }
2911
2912
    /**
2913
     * Return list routes with permissions name.
2914
     *
2915
     * @return array<string, string>
0 ignored issues
show
Documentation introduced by
The doc-type array<string, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
2916
     */
2917
    protected function getAccess()
2918
    {
2919
        $access = array_merge([
2920
            'acl' => 'MASTER',
2921
            'export' => 'EXPORT',
2922
            'historyCompareRevisions' => 'EDIT',
2923
            'historyViewRevision' => 'EDIT',
2924
            'history' => 'EDIT',
2925
            'edit' => 'EDIT',
2926
            'show' => 'VIEW',
2927
            'create' => 'CREATE',
2928
            'delete' => 'DELETE',
2929
            'batchDelete' => 'DELETE',
2930
            'list' => 'LIST',
2931
        ], $this->getAccessMapping());
2932
2933
        foreach ($this->extensions as $extension) {
2934
            $access = array_merge($access, $extension->getAccessMapping($this));
2935
        }
2936
2937
        return $access;
2938
    }
2939
2940
    /**
2941
     * Configures a list of default filters.
2942
     */
2943
    protected function configureDefaultFilterValues(array &$filterValues): void
2944
    {
2945
    }
2946
2947
    /**
2948
     * {@inheritdoc}
2949
     */
2950
    private function buildDatagrid(): void
2951
    {
2952
        if ($this->datagrid) {
2953
            return;
2954
        }
2955
2956
        $filterParameters = $this->getFilterParameters();
2957
2958
        // transform _sort_by from a string to a FieldDescriptionInterface for the datagrid.
2959
        if (isset($filterParameters['_sort_by']) && \is_string($filterParameters['_sort_by'])) {
2960
            if ($this->hasListFieldDescription($filterParameters['_sort_by'])) {
2961
                $filterParameters['_sort_by'] = $this->getListFieldDescription($filterParameters['_sort_by']);
2962
            } else {
2963
                $filterParameters['_sort_by'] = $this->getModelManager()->getNewFieldDescriptionInstance(
2964
                    $this->getClass(),
2965
                    $filterParameters['_sort_by'],
2966
                    []
2967
                );
2968
2969
                $this->getListBuilder()->buildField(null, $filterParameters['_sort_by'], $this);
2970
            }
2971
        }
2972
2973
        // initialize the datagrid
2974
        $this->datagrid = $this->getDatagridBuilder()->getBaseDatagrid($this, $filterParameters);
2975
2976
        $this->datagrid->getPager()->setMaxPageLinks($this->maxPageLinks);
2977
2978
        $mapper = new DatagridMapper($this->getDatagridBuilder(), $this->datagrid, $this);
2979
2980
        // build the datagrid filter
2981
        $this->configureDatagridFilters($mapper);
2982
2983
        // ok, try to limit to add parent filter
2984
        if ($this->isChild() && $this->getParentAssociationMapping() && !$mapper->has($this->getParentAssociationMapping())) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getParentAssociationMapping() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null 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...
2985
            $mapper->add($this->getParentAssociationMapping(), null, [
2986
                'show_filter' => false,
2987
                'label' => false,
2988
                'field_type' => ModelHiddenType::class,
2989
                'field_options' => [
2990
                    'model_manager' => $this->getModelManager(),
2991
                ],
2992
                'operator_type' => HiddenType::class,
2993
            ], null, null, [
2994
                'admin_code' => $this->getParent()->getCode(),
2995
            ]);
2996
        }
2997
2998
        foreach ($this->getExtensions() as $extension) {
2999
            $extension->configureDatagridFilters($mapper);
3000
        }
3001
    }
3002
3003
    /**
3004
     * Build all the related urls to the current admin.
3005
     */
3006
    private function buildRoutes(): void
3007
    {
3008
        if ($this->loaded['routes']) {
3009
            return;
3010
        }
3011
3012
        $this->loaded['routes'] = true;
3013
3014
        $this->routes = new RouteCollection(
3015
            $this->getBaseCodeRoute(),
3016
            $this->getBaseRouteName(),
3017
            $this->getBaseRoutePattern(),
3018
            $this->getBaseControllerName()
3019
        );
3020
3021
        $this->routeBuilder->build($this, $this->routes);
3022
3023
        $this->configureRoutes($this->routes);
3024
3025
        foreach ($this->getExtensions() as $extension) {
3026
            $extension->configureRoutes($this, $this->routes);
3027
        }
3028
    }
3029
}
3030
3031
class_exists(\Sonata\Form\Validator\ErrorElement::class);
3032