Completed
Pull Request — master (#5076)
by Grégoire
03:49
created

AbstractAdmin::addChild()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 31
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 31
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 17
nc 5
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 as MenuFactoryInterface;
18
use Knp\Menu\ItemInterface;
19
use Knp\Menu\ItemInterface as MenuItemInterface;
20
use Sonata\AdminBundle\Builder\DatagridBuilderInterface;
21
use Sonata\AdminBundle\Builder\FormContractorInterface;
22
use Sonata\AdminBundle\Builder\ListBuilderInterface;
23
use Sonata\AdminBundle\Builder\RouteBuilderInterface;
24
use Sonata\AdminBundle\Builder\ShowBuilderInterface;
25
use Sonata\AdminBundle\Datagrid\DatagridInterface;
26
use Sonata\AdminBundle\Datagrid\DatagridMapper;
27
use Sonata\AdminBundle\Datagrid\ListMapper;
28
use Sonata\AdminBundle\Datagrid\Pager;
29
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
30
use Sonata\AdminBundle\Filter\Persister\FilterPersisterInterface;
31
use Sonata\AdminBundle\Form\FormMapper;
32
use Sonata\AdminBundle\Form\Type\ModelHiddenType;
33
use Sonata\AdminBundle\Model\ModelManagerInterface;
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\CoreBundle\Model\Metadata;
42
use Sonata\CoreBundle\Validator\Constraints\InlineConstraint;
43
use Sonata\CoreBundle\Validator\ErrorElement;
44
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
45
use Symfony\Component\Form\Form;
46
use Symfony\Component\Form\FormBuilderInterface;
47
use Symfony\Component\Form\FormEvent;
48
use Symfony\Component\Form\FormEvents;
49
use Symfony\Component\HttpFoundation\Request;
50
use Symfony\Component\PropertyAccess\PropertyPath;
51
use Symfony\Component\Routing\Generator\UrlGeneratorInterface as RoutingUrlGeneratorInterface;
52
use Symfony\Component\Security\Acl\Model\DomainObjectInterface;
53
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
54
use Symfony\Component\Translation\TranslatorInterface;
55
use Symfony\Component\Validator\Validator\ValidatorInterface;
56
57
/**
58
 * @author Thomas Rabaix <[email protected]>
59
 */
60
abstract class AbstractAdmin implements AdminInterface, DomainObjectInterface, AdminTreeInterface
61
{
62
    public const CONTEXT_MENU = 'menu';
63
    public const CONTEXT_DASHBOARD = 'dashboard';
64
65
    public const CLASS_REGEX =
66
        '@
67
        (?:([A-Za-z0-9]*)\\\)?        # vendor name / app name
68
        (Bundle\\\)?                  # optional bundle directory
69
        ([A-Za-z0-9]+?)(?:Bundle)?\\\ # bundle name, with optional suffix
70
        (
71
            Entity|Document|Model|PHPCR|CouchDocument|Phpcr|
72
            Doctrine\\\Orm|Doctrine\\\Phpcr|Doctrine\\\MongoDB|Doctrine\\\CouchDB
73
        )\\\(.*)@x';
74
75
    public const MOSAIC_ICON_CLASS = 'fa fa-th-large fa-fw';
76
77
    /**
78
     * The list FieldDescription constructed from the configureListField method.
79
     *
80
     * @var array
81
     */
82
    protected $listFieldDescriptions = [];
83
84
    /**
85
     * The show FieldDescription constructed from the configureShowFields method.
86
     *
87
     * @var array
88
     */
89
    protected $showFieldDescriptions = [];
90
91
    /**
92
     * The list FieldDescription constructed from the configureFormField method.
93
     *
94
     * @var array
95
     */
96
    protected $formFieldDescriptions = [];
97
98
    /**
99
     * The filter FieldDescription constructed from the configureFilterField method.
100
     *
101
     * @var array
102
     */
103
    protected $filterFieldDescriptions = [];
104
105
    /**
106
     * The number of result to display in the list.
107
     *
108
     * @var int
109
     */
110
    protected $maxPerPage = 32;
111
112
    /**
113
     * The maximum number of page numbers to display in the list.
114
     *
115
     * @var int
116
     */
117
    protected $maxPageLinks = 25;
118
119
    /**
120
     * The base route name used to generate the routing information.
121
     *
122
     * @var string
123
     */
124
    protected $baseRouteName;
125
126
    /**
127
     * The base route pattern used to generate the routing information.
128
     *
129
     * @var string
130
     */
131
    protected $baseRoutePattern;
132
133
    /**
134
     * The base name controller used to generate the routing information.
135
     *
136
     * @var string
137
     */
138
    protected $baseControllerName;
139
140
    /**
141
     * The label class name  (used in the title/breadcrumb ...).
142
     *
143
     * @var string
144
     */
145
    protected $classnameLabel;
146
147
    /**
148
     * The translation domain to be used to translate messages.
149
     *
150
     * @var string
151
     */
152
    protected $translationDomain = 'messages';
153
154
    /**
155
     * Options to set to the form (ie, validation_groups).
156
     *
157
     * @var array
158
     */
159
    protected $formOptions = [];
160
161
    /**
162
     * Default values to the datagrid.
163
     *
164
     * @var array
165
     */
166
    protected $datagridValues = [
167
        '_page' => 1,
168
        '_per_page' => 32,
169
    ];
170
171
    /**
172
     * Predefined per page options.
173
     *
174
     * @var array
175
     */
176
    protected $perPageOptions = [16, 32, 64, 128, 256];
177
178
    /**
179
     * Pager type.
180
     *
181
     * @var string
182
     */
183
    protected $pagerType = Pager::TYPE_DEFAULT;
184
185
    /**
186
     * The code related to the admin.
187
     *
188
     * @var string
189
     */
190
    protected $code;
191
192
    /**
193
     * The label.
194
     *
195
     * @var string
196
     */
197
    protected $label;
198
199
    /**
200
     * Whether or not to persist the filters in the session.
201
     *
202
     * NEXT_MAJOR: remove this property
203
     *
204
     * @var bool
205
     *
206
     * @deprecated since 3.34, to be removed in 4.0.
207
     */
208
    protected $persistFilters = false;
209
210
    /**
211
     * Array of routes related to this admin.
212
     *
213
     * @var RouteCollection
214
     */
215
    protected $routes;
216
217
    /**
218
     * The subject only set in edit/update/create mode.
219
     *
220
     * @var object
221
     */
222
    protected $subject;
223
224
    /**
225
     * Define a Collection of child admin, ie /admin/order/{id}/order-element/{childId}.
226
     *
227
     * @var array
228
     */
229
    protected $children = [];
230
231
    /**
232
     * Reference the parent collection.
233
     *
234
     * @var AdminInterface|null
235
     */
236
    protected $parent = null;
237
238
    /**
239
     * The related parent association, ie if OrderElement has a parent property named order,
240
     * then the $parentAssociationMapping must be a string named `order`.
241
     *
242
     * NEXT_MAJOR: remove this attribute.
243
     *
244
     * @deprecated This attribute is deprecated since 3.24 and will be removed in 4.0
245
     *
246
     * @var string
247
     */
248
    protected $baseCodeRoute = '';
249
250
    /**
251
     * NEXT_MAJOR: should be default array and private.
252
     *
253
     * @var string|array
254
     */
255
    protected $parentAssociationMapping = null;
256
257
    /**
258
     * Reference the parent FieldDescription related to this admin
259
     * only set for FieldDescription which is associated to an Sub Admin instance.
260
     *
261
     * @var FieldDescriptionInterface
262
     */
263
    protected $parentFieldDescription;
264
265
    /**
266
     * If true then the current admin is part of the nested admin set (from the url).
267
     *
268
     * @var bool
269
     */
270
    protected $currentChild = false;
271
272
    /**
273
     * The uniqid is used to avoid clashing with 2 admin related to the code
274
     * ie: a Block linked to a Block.
275
     *
276
     * @var string
277
     */
278
    protected $uniqid;
279
280
    /**
281
     * The Entity or Document manager.
282
     *
283
     * @var ModelManagerInterface
284
     */
285
    protected $modelManager;
286
287
    /**
288
     * The current request object.
289
     *
290
     * @var \Symfony\Component\HttpFoundation\Request
291
     */
292
    protected $request;
293
294
    /**
295
     * The translator component.
296
     *
297
     * NEXT_MAJOR: remove this property
298
     *
299
     * @var \Symfony\Component\Translation\TranslatorInterface
300
     *
301
     * @deprecated since 3.9, to be removed with 4.0
302
     */
303
    protected $translator;
304
305
    /**
306
     * The related form contractor.
307
     *
308
     * @var FormContractorInterface
309
     */
310
    protected $formContractor;
311
312
    /**
313
     * The related list builder.
314
     *
315
     * @var ListBuilderInterface
316
     */
317
    protected $listBuilder;
318
319
    /**
320
     * The related view builder.
321
     *
322
     * @var ShowBuilderInterface
323
     */
324
    protected $showBuilder;
325
326
    /**
327
     * The related datagrid builder.
328
     *
329
     * @var DatagridBuilderInterface
330
     */
331
    protected $datagridBuilder;
332
333
    /**
334
     * @var RouteBuilderInterface
335
     */
336
    protected $routeBuilder;
337
338
    /**
339
     * The datagrid instance.
340
     *
341
     * @var \Sonata\AdminBundle\Datagrid\DatagridInterface
342
     */
343
    protected $datagrid;
344
345
    /**
346
     * The router instance.
347
     *
348
     * @var RouteGeneratorInterface
349
     */
350
    protected $routeGenerator;
351
352
    /**
353
     * @var SecurityHandlerInterface
354
     */
355
    protected $securityHandler = null;
356
357
    /**
358
     * @var ValidatorInterface
359
     */
360
    protected $validator = null;
361
362
    /**
363
     * The configuration pool.
364
     *
365
     * @var Pool
366
     */
367
    protected $configurationPool;
368
369
    /**
370
     * @var MenuItemInterface
371
     */
372
    protected $menu;
373
374
    /**
375
     * @var MenuFactoryInterface
376
     */
377
    protected $menuFactory;
378
379
    /**
380
     * @var array
381
     */
382
    protected $loaded = [
383
        'view_fields' => false,
384
        'view_groups' => false,
385
        'routes' => false,
386
        'tab_menu' => false,
387
    ];
388
389
    /**
390
     * @var array
391
     */
392
    protected $formTheme = [];
393
394
    /**
395
     * @var array
396
     */
397
    protected $filterTheme = [];
398
399
    /**
400
     * @var array
401
     *
402
     * @deprecated since 3.34, will be dropped in 4.0. Use TemplateRegistry services instead
403
     */
404
    protected $templates = [];
405
406
    /**
407
     * @var AdminExtensionInterface[]
408
     */
409
    protected $extensions = [];
410
411
    /**
412
     * @var LabelTranslatorStrategyInterface
413
     */
414
    protected $labelTranslatorStrategy;
415
416
    /**
417
     * Setting to true will enable preview mode for
418
     * the entity and show a preview button in the
419
     * edit/create forms.
420
     *
421
     * @var bool
422
     */
423
    protected $supportsPreviewMode = false;
424
425
    /**
426
     * Roles and permissions per role.
427
     *
428
     * @var array 'role' => ['permission', 'permission']
429
     */
430
    protected $securityInformation = [];
431
432
    protected $cacheIsGranted = [];
433
434
    /**
435
     * Action list for the search result.
436
     *
437
     * @var string[]
438
     */
439
    protected $searchResultActions = ['edit', 'show'];
440
441
    protected $listModes = [
442
        'list' => [
443
            'class' => 'fa fa-list fa-fw',
444
        ],
445
        'mosaic' => [
446
            'class' => self::MOSAIC_ICON_CLASS,
447
        ],
448
    ];
449
450
    /**
451
     * The Access mapping.
452
     *
453
     * @var array [action1 => requiredRole1, action2 => [requiredRole2, requiredRole3]]
454
     */
455
    protected $accessMapping = [];
456
457
    /**
458
     * @var MutableTemplateRegistryInterface
459
     */
460
    private $templateRegistry;
461
462
    /**
463
     * The class name managed by the admin class.
464
     *
465
     * @var string
466
     */
467
    private $class;
468
469
    /**
470
     * The subclasses supported by the admin class.
471
     *
472
     * @var array
473
     */
474
    private $subClasses = [];
475
476
    /**
477
     * The list collection.
478
     *
479
     * @var array
480
     */
481
    private $list;
482
483
    /**
484
     * @var FieldDescriptionCollection
485
     */
486
    private $show;
487
488
    /**
489
     * @var Form
490
     */
491
    private $form;
492
493
    /**
494
     * @var DatagridInterface
495
     */
496
    private $filter;
497
498
    /**
499
     * The cached base route name.
500
     *
501
     * @var string
502
     */
503
    private $cachedBaseRouteName;
504
505
    /**
506
     * The cached base route pattern.
507
     *
508
     * @var string
509
     */
510
    private $cachedBaseRoutePattern;
511
512
    /**
513
     * The form group disposition.
514
     *
515
     * @var array|bool
516
     */
517
    private $formGroups = false;
518
519
    /**
520
     * The form tabs disposition.
521
     *
522
     * @var array|bool
523
     */
524
    private $formTabs = false;
525
526
    /**
527
     * The view group disposition.
528
     *
529
     * @var array|bool
530
     */
531
    private $showGroups = false;
532
533
    /**
534
     * The view tab disposition.
535
     *
536
     * @var array|bool
537
     */
538
    private $showTabs = false;
539
540
    /**
541
     * The manager type to use for the admin.
542
     *
543
     * @var string
544
     */
545
    private $managerType;
546
547
    /**
548
     * Component responsible for persisting filters.
549
     *
550
     * @var FilterPersisterInterface|null
551
     */
552
    private $filterPersister;
553
554
    /**
555
     * @param string $code
556
     * @param string $class
557
     * @param string $baseControllerName
558
     */
559
    public function __construct($code, $class, $baseControllerName)
560
    {
561
        $this->code = $code;
562
        $this->class = $class;
563
        $this->baseControllerName = $baseControllerName;
564
565
        $this->predefinePerPageOptions();
566
        $this->datagridValues['_per_page'] = $this->maxPerPage;
567
    }
568
569
    /**
570
     * {@inheritdoc}
571
     *
572
     * NEXT_MAJOR: return null to indicate no override
573
     */
574
    public function getExportFormats()
575
    {
576
        return [
577
            'json', 'xml', 'csv', 'xls',
578
        ];
579
    }
580
581
    /**
582
     * {@inheritdoc}
583
     */
584
    public function getExportFields()
585
    {
586
        $fields = $this->getModelManager()->getExportFields($this->getClass());
587
588
        foreach ($this->getExtensions() as $extension) {
589
            if (method_exists($extension, 'configureExportFields')) {
590
                $fields = $extension->configureExportFields($this, $fields);
591
            }
592
        }
593
594
        return $fields;
595
    }
596
597
    public function getDataSourceIterator()
598
    {
599
        $datagrid = $this->getDatagrid();
600
        $datagrid->buildPager();
601
602
        $fields = [];
603
604
        foreach ($this->getExportFields() as $key => $field) {
605
            $label = $this->getTranslationLabel($field, 'export', 'label');
606
            $transLabel = $this->trans($label);
607
608
            // NEXT_MAJOR: Remove this hack, because all field labels will be translated with the major release
609
            // No translation key exists
610
            if ($transLabel == $label) {
611
                $fields[$key] = $field;
612
            } else {
613
                $fields[$transLabel] = $field;
614
            }
615
        }
616
617
        return $this->getModelManager()->getDataSourceIterator($datagrid, $fields);
618
    }
619
620
    public function validate(ErrorElement $errorElement, $object): void
621
    {
622
    }
623
624
    /**
625
     * define custom variable.
626
     */
627
    public function initialize(): void
628
    {
629
        if (!$this->classnameLabel) {
630
            $this->classnameLabel = substr($this->getClass(), strrpos($this->getClass(), '\\') + 1);
631
        }
632
633
        $this->configure();
634
    }
635
636
    public function update($object)
637
    {
638
        $this->preUpdate($object);
639
        foreach ($this->extensions as $extension) {
640
            $extension->preUpdate($this, $object);
641
        }
642
643
        $result = $this->getModelManager()->update($object);
644
        // BC compatibility
645
        if (null !== $result) {
646
            $object = $result;
647
        }
648
649
        $this->postUpdate($object);
650
        foreach ($this->extensions as $extension) {
651
            $extension->postUpdate($this, $object);
652
        }
653
654
        return $object;
655
    }
656
657
    public function create($object)
658
    {
659
        $this->prePersist($object);
660
        foreach ($this->extensions as $extension) {
661
            $extension->prePersist($this, $object);
662
        }
663
664
        $result = $this->getModelManager()->create($object);
665
        // BC compatibility
666
        if (null !== $result) {
667
            $object = $result;
668
        }
669
670
        $this->postPersist($object);
671
        foreach ($this->extensions as $extension) {
672
            $extension->postPersist($this, $object);
673
        }
674
675
        $this->createObjectSecurity($object);
676
677
        return $object;
678
    }
679
680
    public function delete($object): void
681
    {
682
        $this->preRemove($object);
683
        foreach ($this->extensions as $extension) {
684
            $extension->preRemove($this, $object);
685
        }
686
687
        $this->getSecurityHandler()->deleteObjectSecurity($this, $object);
688
        $this->getModelManager()->delete($object);
689
690
        $this->postRemove($object);
691
        foreach ($this->extensions as $extension) {
692
            $extension->postRemove($this, $object);
693
        }
694
    }
695
696
    public function preValidate($object): void
697
    {
698
    }
699
700
    public function preUpdate($object): void
701
    {
702
    }
703
704
    public function postUpdate($object): void
705
    {
706
    }
707
708
    public function prePersist($object): void
709
    {
710
    }
711
712
    public function postPersist($object): void
713
    {
714
    }
715
716
    public function preRemove($object): void
717
    {
718
    }
719
720
    public function postRemove($object): void
721
    {
722
    }
723
724
    public function preBatchAction($actionName, ProxyQueryInterface $query, array &$idx, $allElements): void
725
    {
726
    }
727
728
    public function getFilterParameters()
729
    {
730
        $parameters = [];
731
732
        // build the values array
733
        if ($this->hasRequest()) {
734
            $filters = $this->request->query->get('filter', []);
735
736
            // if filter persistence is configured
737
            // NEXT_MAJOR: remove `$this->persistFilters !== false` from the condition
738
            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 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...
739
                // if reset filters is asked, remove from storage
740
                if ('reset' === $this->request->query->get('filters')) {
741
                    $this->filterPersister->reset($this->getCode());
742
                }
743
744
                // if no filters, fetch from storage
745
                // otherwise save to storage
746
                if (empty($filters)) {
747
                    $filters = $this->filterPersister->get($this->getCode());
748
                } else {
749
                    $this->filterPersister->set($this->getCode(), $filters);
750
                }
751
            }
752
753
            $parameters = array_merge(
754
                $this->getModelManager()->getDefaultSortValues($this->getClass()),
755
                $this->datagridValues,
756
                $this->getDefaultFilterValues(),
757
                $filters
758
            );
759
760
            if (!$this->determinedPerPageValue($parameters['_per_page'])) {
761
                $parameters['_per_page'] = $this->maxPerPage;
762
            }
763
764
            // always force the parent value
765
            if ($this->isChild() && $this->getParentAssociationMapping()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getParentAssociationMapping() of type null|string 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...
766
                $name = str_replace('.', '__', $this->getParentAssociationMapping());
767
                $parameters[$name] = ['value' => $this->request->get($this->getParent()->getIdParameter())];
768
            }
769
        }
770
771
        return $parameters;
772
    }
773
774
    /**
775
     * Returns the name of the parent related field, so the field can be use to set the default
776
     * value (ie the parent object) or to filter the object.
777
     *
778
     * @throws \InvalidArgumentException
779
     *
780
     * @return null|string
781
     */
782
    public function getParentAssociationMapping()
783
    {
784
        // NEXT_MAJOR: remove array check
785
        if (\is_array($this->parentAssociationMapping) && $this->getParent()) {
786
            $parent = $this->getParent()->getCode();
787
788
            if (array_key_exists($parent, $this->parentAssociationMapping)) {
789
                return $this->parentAssociationMapping[$parent];
790
            }
791
792
            throw new \InvalidArgumentException(sprintf(
793
                "There's no association between %s and %s.",
794
                $this->getCode(),
795
                $this->getParent()->getCode()
796
            ));
797
        }
798
799
        // NEXT_MAJOR: remove this line
800
        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 800 which is incompatible with the return type documented by Sonata\AdminBundle\Admin...arentAssociationMapping of type null|string.
Loading history...
801
    }
802
803
    /**
804
     * @param string $code
805
     * @param string $value
806
     */
807
    final public function addParentAssociationMapping($code, $value): void
808
    {
809
        $this->parentAssociationMapping[$code] = $value;
810
    }
811
812
    /**
813
     * Returns the baseRoutePattern used to generate the routing information.
814
     *
815
     * @throws \RuntimeException
816
     *
817
     * @return string the baseRoutePattern used to generate the routing information
818
     */
819
    public function getBaseRoutePattern()
820
    {
821
        if (null !== $this->cachedBaseRoutePattern) {
822
            return $this->cachedBaseRoutePattern;
823
        }
824
825
        if ($this->isChild()) { // the admin class is a child, prefix it with the parent route pattern
826
            if (!$this->baseRoutePattern) {
827
                preg_match(self::CLASS_REGEX, $this->class, $matches);
828
829
                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...
830
                    throw new \RuntimeException(sprintf('Please define a default `baseRoutePattern` value for the admin class `%s`', get_class($this)));
831
                }
832
            }
833
834
            $this->cachedBaseRoutePattern = sprintf('%s/%s/%s',
835
                $this->getParent()->getBaseRoutePattern(),
836
                $this->getParent()->getRouterIdParameter(),
837
                $this->baseRoutePattern ?: $this->urlize($matches[5], '-')
0 ignored issues
show
Bug introduced by
The variable $matches does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
838
            );
839
        } elseif ($this->baseRoutePattern) {
840
            $this->cachedBaseRoutePattern = $this->baseRoutePattern;
841
        } else {
842
            preg_match(self::CLASS_REGEX, $this->class, $matches);
843
844
            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...
845
                throw new \RuntimeException(sprintf('Please define a default `baseRoutePattern` value for the admin class `%s`', get_class($this)));
846
            }
847
848
            $this->cachedBaseRoutePattern = sprintf('/%s%s/%s',
849
                empty($matches[1]) ? '' : $this->urlize($matches[1], '-').'/',
850
                $this->urlize($matches[3], '-'),
851
                $this->urlize($matches[5], '-')
852
            );
853
        }
854
855
        return $this->cachedBaseRoutePattern;
856
    }
857
858
    /**
859
     * Returns the baseRouteName used to generate the routing information.
860
     *
861
     * @throws \RuntimeException
862
     *
863
     * @return string the baseRouteName used to generate the routing information
864
     */
865
    public function getBaseRouteName()
866
    {
867
        if (null !== $this->cachedBaseRouteName) {
868
            return $this->cachedBaseRouteName;
869
        }
870
871
        if ($this->isChild()) { // the admin class is a child, prefix it with the parent route name
872
            if (!$this->baseRouteName) {
873
                preg_match(self::CLASS_REGEX, $this->class, $matches);
874
875
                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...
876
                    throw new \RuntimeException(sprintf('Cannot automatically determine base route name, please define a default `baseRouteName` value for the admin class `%s`', get_class($this)));
877
                }
878
            }
879
880
            $this->cachedBaseRouteName = sprintf('%s_%s',
881
                $this->getParent()->getBaseRouteName(),
882
                $this->baseRouteName ?: $this->urlize($matches[5])
0 ignored issues
show
Bug introduced by
The variable $matches does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
883
            );
884
        } elseif ($this->baseRouteName) {
885
            $this->cachedBaseRouteName = $this->baseRouteName;
886
        } else {
887
            preg_match(self::CLASS_REGEX, $this->class, $matches);
888
889
            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...
890
                throw new \RuntimeException(sprintf('Cannot automatically determine base route name, please define a default `baseRouteName` value for the admin class `%s`', get_class($this)));
891
            }
892
893
            $this->cachedBaseRouteName = sprintf('admin_%s%s_%s',
894
                empty($matches[1]) ? '' : $this->urlize($matches[1]).'_',
895
                $this->urlize($matches[3]),
896
                $this->urlize($matches[5])
897
            );
898
        }
899
900
        return $this->cachedBaseRouteName;
901
    }
902
903
    public function getClass()
904
    {
905
        if ($this->hasActiveSubClass()) {
906
            if ($this->getParentFieldDescription()) {
907
                throw new \RuntimeException('Feature not implemented: an embedded admin cannot have subclass');
908
            }
909
910
            $subClass = $this->getRequest()->query->get('subclass');
911
912
            if (!$this->hasSubClass($subClass)) {
913
                throw new \RuntimeException(sprintf('Subclass "%" is not defined.', $subClass));
914
            }
915
916
            return $this->getSubClass($subClass);
917
        }
918
919
        // see https://github.com/sonata-project/SonataCoreBundle/commit/247eeb0a7ca7211142e101754769d70bc402a5b4
920
        if ($this->subject && is_object($this->subject)) {
921
            return ClassUtils::getClass($this->subject);
922
        }
923
924
        return $this->class;
925
    }
926
927
    public function getSubClasses()
928
    {
929
        return $this->subClasses;
930
    }
931
932
    /**
933
     * NEXT_MAJOR: remove this method.
934
     */
935
    public function addSubClass($subClass): void
936
    {
937
        @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...
938
            'Method "%s" is deprecated since 3.30 and will be removed in 4.0.',
939
            __METHOD__
940
        ), E_USER_DEPRECATED);
941
942
        if (!in_array($subClass, $this->subClasses)) {
943
            $this->subClasses[] = $subClass;
944
        }
945
    }
946
947
    public function setSubClasses(array $subClasses): void
948
    {
949
        $this->subClasses = $subClasses;
950
    }
951
952
    public function hasSubClass($name)
953
    {
954
        return isset($this->subClasses[$name]);
955
    }
956
957
    public function hasActiveSubClass()
958
    {
959
        if (count($this->subClasses) > 0 && $this->request) {
960
            return null !== $this->getRequest()->query->get('subclass');
961
        }
962
963
        return false;
964
    }
965
966
    public function getActiveSubClass()
967
    {
968
        if (!$this->hasActiveSubClass()) {
969
            return;
970
        }
971
972
        return $this->getSubClass($this->getActiveSubclassCode());
973
    }
974
975
    public function getActiveSubclassCode()
976
    {
977
        if (!$this->hasActiveSubClass()) {
978
            return;
979
        }
980
981
        $subClass = $this->getRequest()->query->get('subclass');
982
983
        if (!$this->hasSubClass($subClass)) {
984
            return;
985
        }
986
987
        return $subClass;
988
    }
989
990
    public function getBatchActions()
991
    {
992
        $actions = [];
993
994
        if ($this->hasRoute('delete') && $this->hasAccess('delete')) {
995
            $actions['delete'] = [
996
                'label' => 'action_delete',
997
                'translation_domain' => 'SonataAdminBundle',
998
                'ask_confirmation' => true, // by default always true
999
            ];
1000
        }
1001
1002
        $actions = $this->configureBatchActions($actions);
1003
1004
        foreach ($this->getExtensions() as $extension) {
1005
            $actions = $extension->configureBatchActions($this, $actions);
1006
        }
1007
1008
        foreach ($actions  as $name => &$action) {
1009
            if (!array_key_exists('label', $action)) {
1010
                $action['label'] = $this->getTranslationLabel($name, 'batch', 'label');
1011
            }
1012
1013
            if (!array_key_exists('translation_domain', $action)) {
1014
                $action['translation_domain'] = $this->getTranslationDomain();
1015
            }
1016
        }
1017
1018
        return $actions;
1019
    }
1020
1021
    public function getRoutes()
1022
    {
1023
        $this->buildRoutes();
1024
1025
        return $this->routes;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->routes; (Sonata\AdminBundle\Route\RouteCollection) is incompatible with the return type declared by the interface Sonata\AdminBundle\Admin...torInterface::getRoutes of type Sonata\AdminBundle\Admin\RouteCollection.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
1026
    }
1027
1028
    public function getRouterIdParameter()
1029
    {
1030
        return '{'.$this->getIdParameter().'}';
1031
    }
1032
1033
    public function getIdParameter()
1034
    {
1035
        $parameter = 'id';
1036
1037
        for ($i = 0; $i < $this->getChildDepth(); ++$i) {
1038
            $parameter = 'child'.ucfirst($parameter);
1039
        }
1040
1041
        return $parameter;
1042
    }
1043
1044
    public function hasRoute($name)
1045
    {
1046
        if (!$this->routeGenerator) {
1047
            throw new \RuntimeException('RouteGenerator cannot be null');
1048
        }
1049
1050
        return $this->routeGenerator->hasAdminRoute($this, $name);
1051
    }
1052
1053
    public function isCurrentRoute($name, $adminCode = null)
1054
    {
1055
        if (!$this->hasRequest()) {
1056
            return false;
1057
        }
1058
1059
        $request = $this->getRequest();
1060
        $route = $request->get('_route');
1061
1062
        if ($adminCode) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $adminCode 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...
1063
            $admin = $this->getConfigurationPool()->getAdminByAdminCode($adminCode);
1064
        } else {
1065
            $admin = $this;
1066
        }
1067
1068
        if (!$admin) {
1069
            return false;
1070
        }
1071
1072
        return ($admin->getBaseRouteName().'_'.$name) == $route;
1073
    }
1074
1075
    public function generateObjectUrl($name, $object, array $parameters = [], $absolute = RoutingUrlGeneratorInterface::ABSOLUTE_PATH)
1076
    {
1077
        $parameters['id'] = $this->getUrlsafeIdentifier($object);
1078
1079
        return $this->generateUrl($name, $parameters, $absolute);
1080
    }
1081
1082
    public function generateUrl($name, array $parameters = [], $absolute = RoutingUrlGeneratorInterface::ABSOLUTE_PATH)
1083
    {
1084
        return $this->routeGenerator->generateUrl($this, $name, $parameters, $absolute);
0 ignored issues
show
Documentation introduced by
$absolute is of type integer, but the function expects a boolean.

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...
1085
    }
1086
1087
    public function generateMenuUrl($name, array $parameters = [], $absolute = RoutingUrlGeneratorInterface::ABSOLUTE_PATH)
1088
    {
1089
        return $this->routeGenerator->generateMenuUrl($this, $name, $parameters, $absolute);
0 ignored issues
show
Documentation introduced by
$absolute is of type integer, but the function expects a boolean.

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...
1090
    }
1091
1092
    final public function setTemplateRegistry(MutableTemplateRegistryInterface $templateRegistry): void
1093
    {
1094
        $this->templateRegistry = $templateRegistry;
1095
    }
1096
1097
    public function setTemplates(array $templates): void
1098
    {
1099
        // NEXT_MAJOR: Remove this line
1100
        $this->templates = $templates;
0 ignored issues
show
Deprecated Code introduced by
The property Sonata\AdminBundle\Admin\AbstractAdmin::$templates has been deprecated with message: since 3.34, will be dropped in 4.0. Use TemplateRegistry services instead

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...
1101
1102
        $this->getTemplateRegistry()->setTemplates($templates);
1103
    }
1104
1105
    /**
1106
     * {@inheritdoc}
1107
     */
1108
    public function setTemplate($name, $template): void
1109
    {
1110
        // NEXT_MAJOR: Remove this line
1111
        $this->templates[$name] = $template;
0 ignored issues
show
Deprecated Code introduced by
The property Sonata\AdminBundle\Admin\AbstractAdmin::$templates has been deprecated with message: since 3.34, will be dropped in 4.0. Use TemplateRegistry services instead

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...
1112
1113
        $this->getTemplateRegistry()->setTemplate($name, $template);
1114
    }
1115
1116
    /**
1117
     * @deprecated since 3.34, will be dropped in 4.0. Use TemplateRegistry services instead
1118
     *
1119
     * @return array
1120
     */
1121
    public function getTemplates(): array
1122
    {
1123
        return $this->getTemplateRegistry()->getTemplates();
1124
    }
1125
1126
    /**
1127
     * @deprecated since 3.34, will be dropped in 4.0. Use TemplateRegistry services instead
1128
     *
1129
     * @param string $name
1130
     *
1131
     * @return null|string
1132
     */
1133
    public function getTemplate($name)
1134
    {
1135
        return $this->getTemplateRegistry()->getTemplate($name);
1136
    }
1137
1138
    public function getNewInstance()
1139
    {
1140
        $object = $this->getModelManager()->getModelInstance($this->getClass());
1141
        foreach ($this->getExtensions() as $extension) {
1142
            $extension->alterNewInstance($this, $object);
1143
        }
1144
1145
        return $object;
1146
    }
1147
1148
    public function getFormBuilder()
1149
    {
1150
        $this->formOptions['data_class'] = $this->getClass();
1151
1152
        $formBuilder = $this->getFormContractor()->getFormBuilder(
1153
            $this->getUniqid(),
1154
            $this->formOptions
1155
        );
1156
1157
        $this->defineFormBuilder($formBuilder);
1158
1159
        return $formBuilder;
1160
    }
1161
1162
    /**
1163
     * This method is being called by the main admin class and the child class,
1164
     * the getFormBuilder is only call by the main admin class.
1165
     */
1166
    public function defineFormBuilder(FormBuilderInterface $formBuilder): void
1167
    {
1168
        $mapper = new FormMapper($this->getFormContractor(), $formBuilder, $this);
1169
1170
        $this->configureFormFields($mapper);
1171
1172
        foreach ($this->getExtensions() as $extension) {
1173
            $extension->configureFormFields($mapper);
1174
        }
1175
1176
        $this->attachInlineValidator();
1177
    }
1178
1179
    public function attachAdminClass(FieldDescriptionInterface $fieldDescription): void
1180
    {
1181
        $pool = $this->getConfigurationPool();
1182
1183
        $adminCode = $fieldDescription->getOption('admin_code');
1184
1185
        if (null !== $adminCode) {
1186
            $admin = $pool->getAdminByAdminCode($adminCode);
0 ignored issues
show
Documentation introduced by
$adminCode is of type array, 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...
1187
        } else {
1188
            $admin = $pool->getAdminByClass($fieldDescription->getTargetEntity());
1189
        }
1190
1191
        if (!$admin) {
1192
            return;
1193
        }
1194
1195
        if ($this->hasRequest()) {
1196
            $admin->setRequest($this->getRequest());
1197
        }
1198
1199
        $fieldDescription->setAssociationAdmin($admin);
1200
    }
1201
1202
    public function getObject($id)
1203
    {
1204
        $object = $this->getModelManager()->find($this->getClass(), $id);
1205
        foreach ($this->getExtensions() as $extension) {
1206
            $extension->alterObject($this, $object);
1207
        }
1208
1209
        return $object;
1210
    }
1211
1212
    public function getForm()
1213
    {
1214
        $this->buildForm();
1215
1216
        return $this->form;
1217
    }
1218
1219
    public function getList()
1220
    {
1221
        $this->buildList();
1222
1223
        return $this->list;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->list; (array) is incompatible with the return type declared by the interface Sonata\AdminBundle\Admin...istryInterface::getList of type Sonata\AdminBundle\Admin...ldDescriptionCollection.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
1224
    }
1225
1226
    public function createQuery($context = 'list')
1227
    {
1228
        if (func_num_args() > 0) {
1229
            @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...
1230
                'The $context argument of '.__METHOD__.' is deprecated since 3.3, to be removed in 4.0.',
1231
                E_USER_DEPRECATED
1232
            );
1233
        }
1234
        $query = $this->getModelManager()->createQuery($this->getClass());
1235
1236
        foreach ($this->extensions as $extension) {
1237
            $extension->configureQuery($this, $query, $context);
1238
        }
1239
1240
        return $query;
1241
    }
1242
1243
    public function getDatagrid()
1244
    {
1245
        $this->buildDatagrid();
1246
1247
        return $this->datagrid;
1248
    }
1249
1250
    public function buildTabMenu($action, AdminInterface $childAdmin = null): void
1251
    {
1252
        if ($this->loaded['tab_menu']) {
1253
            return;
1254
        }
1255
1256
        $this->loaded['tab_menu'] = true;
1257
1258
        $menu = $this->menuFactory->createItem('root');
1259
        $menu->setChildrenAttribute('class', 'nav navbar-nav');
1260
        $menu->setExtra('translation_domain', $this->translationDomain);
1261
1262
        // Prevents BC break with KnpMenuBundle v1.x
1263
        if (method_exists($menu, 'setCurrentUri')) {
1264
            $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...
1265
        }
1266
1267
        $this->configureTabMenu($menu, $action, $childAdmin);
1268
1269
        foreach ($this->getExtensions() as $extension) {
1270
            $extension->configureTabMenu($this, $menu, $action, $childAdmin);
1271
        }
1272
1273
        $this->menu = $menu;
1274
    }
1275
1276
    /**
1277
     * @param string $action
1278
     *
1279
     * @return ItemInterface
1280
     */
1281
    public function getSideMenu($action, AdminInterface $childAdmin = null)
1282
    {
1283
        if ($this->isChild()) {
1284
            return $this->getParent()->getSideMenu($action, $this);
1285
        }
1286
1287
        $this->buildTabMenu($action, $childAdmin);
1288
1289
        return $this->menu;
1290
    }
1291
1292
    /**
1293
     * Returns the root code.
1294
     *
1295
     * @return string the root code
1296
     */
1297
    public function getRootCode()
1298
    {
1299
        return $this->getRoot()->getCode();
1300
    }
1301
1302
    /**
1303
     * Returns the master admin.
1304
     *
1305
     * @return AbstractAdmin the root admin class
1306
     */
1307
    public function getRoot()
1308
    {
1309
        $parentFieldDescription = $this->getParentFieldDescription();
1310
1311
        if (!$parentFieldDescription) {
1312
            return $this;
1313
        }
1314
1315
        return $parentFieldDescription->getAdmin()->getRoot();
1316
    }
1317
1318
    public function setBaseControllerName($baseControllerName): void
1319
    {
1320
        $this->baseControllerName = $baseControllerName;
1321
    }
1322
1323
    public function getBaseControllerName()
1324
    {
1325
        return $this->baseControllerName;
1326
    }
1327
1328
    /**
1329
     * @param string $label
1330
     */
1331
    public function setLabel($label): void
1332
    {
1333
        $this->label = $label;
1334
    }
1335
1336
    public function getLabel()
1337
    {
1338
        return $this->label;
1339
    }
1340
1341
    /**
1342
     * @param bool $persist
1343
     *
1344
     * NEXT_MAJOR: remove this method
1345
     *
1346
     * @deprecated since 3.34, to be removed in 4.0.
1347
     */
1348
    public function setPersistFilters($persist): void
1349
    {
1350
        @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...
1351
            'The '.__METHOD__.' method is deprecated since version 3.34 and will be removed in 4.0.',
1352
            E_USER_DEPRECATED
1353
        );
1354
1355
        $this->persistFilters = $persist;
0 ignored issues
show
Deprecated Code introduced by
The property Sonata\AdminBundle\Admin...tAdmin::$persistFilters has been deprecated with message: since 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...
1356
    }
1357
1358
    /**
1359
     * @param FilterPersisterInterface|null $filterPersister
1360
     */
1361
    public function setFilterPersister(FilterPersisterInterface $filterPersister = null): void
1362
    {
1363
        $this->filterPersister = $filterPersister;
1364
        // NEXT_MAJOR remove the deprecated property will be removed. Needed for persisted filter condition.
1365
        $this->persistFilters = true;
0 ignored issues
show
Deprecated Code introduced by
The property Sonata\AdminBundle\Admin...tAdmin::$persistFilters has been deprecated with message: since 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...
1366
    }
1367
1368
    /**
1369
     * @param int $maxPerPage
1370
     */
1371
    public function setMaxPerPage($maxPerPage): void
1372
    {
1373
        $this->maxPerPage = $maxPerPage;
1374
    }
1375
1376
    /**
1377
     * @return int
1378
     */
1379
    public function getMaxPerPage()
1380
    {
1381
        return $this->maxPerPage;
1382
    }
1383
1384
    /**
1385
     * @param int $maxPageLinks
1386
     */
1387
    public function setMaxPageLinks($maxPageLinks): void
1388
    {
1389
        $this->maxPageLinks = $maxPageLinks;
1390
    }
1391
1392
    /**
1393
     * @return int
1394
     */
1395
    public function getMaxPageLinks()
1396
    {
1397
        return $this->maxPageLinks;
1398
    }
1399
1400
    public function getFormGroups()
1401
    {
1402
        return $this->formGroups;
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->formGroups; of type array|boolean adds the type boolean to the return on line 1402 which is incompatible with the return type declared by the interface Sonata\AdminBundle\Admin...nterface::getFormGroups of type array.
Loading history...
1403
    }
1404
1405
    public function setFormGroups(array $formGroups): void
1406
    {
1407
        $this->formGroups = $formGroups;
1408
    }
1409
1410
    public function removeFieldFromFormGroup($key): void
1411
    {
1412
        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...
1413
            unset($this->formGroups[$name]['fields'][$key]);
1414
1415
            if (empty($this->formGroups[$name]['fields'])) {
1416
                unset($this->formGroups[$name]);
1417
            }
1418
        }
1419
    }
1420
1421
    /**
1422
     * @param array $group
1423
     */
1424
    public function reorderFormGroup($group, array $keys): void
1425
    {
1426
        $formGroups = $this->getFormGroups();
1427
        $formGroups[$group]['fields'] = array_merge(array_flip($keys), $formGroups[$group]['fields']);
1428
        $this->setFormGroups($formGroups);
0 ignored issues
show
Bug introduced by
It seems like $formGroups defined by $this->getFormGroups() on line 1426 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...
1429
    }
1430
1431
    public function getFormTabs()
1432
    {
1433
        return $this->formTabs;
1434
    }
1435
1436
    public function setFormTabs(array $formTabs): void
1437
    {
1438
        $this->formTabs = $formTabs;
1439
    }
1440
1441
    public function getShowTabs()
1442
    {
1443
        return $this->showTabs;
1444
    }
1445
1446
    public function setShowTabs(array $showTabs): void
1447
    {
1448
        $this->showTabs = $showTabs;
1449
    }
1450
1451
    public function getShowGroups()
1452
    {
1453
        return $this->showGroups;
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->showGroups; of type array|boolean adds the type boolean to the return on line 1453 which is incompatible with the return type declared by the interface Sonata\AdminBundle\Admin...nterface::getShowGroups of type array.
Loading history...
1454
    }
1455
1456
    public function setShowGroups(array $showGroups): void
1457
    {
1458
        $this->showGroups = $showGroups;
1459
    }
1460
1461
    public function reorderShowGroup($group, array $keys): void
1462
    {
1463
        $showGroups = $this->getShowGroups();
1464
        $showGroups[$group]['fields'] = array_merge(array_flip($keys), $showGroups[$group]['fields']);
1465
        $this->setShowGroups($showGroups);
0 ignored issues
show
Bug introduced by
It seems like $showGroups defined by $this->getShowGroups() on line 1463 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...
1466
    }
1467
1468
    public function setParentFieldDescription(FieldDescriptionInterface $parentFieldDescription): void
1469
    {
1470
        $this->parentFieldDescription = $parentFieldDescription;
1471
    }
1472
1473
    public function getParentFieldDescription()
1474
    {
1475
        return $this->parentFieldDescription;
1476
    }
1477
1478
    public function hasParentFieldDescription()
1479
    {
1480
        return $this->parentFieldDescription instanceof FieldDescriptionInterface;
1481
    }
1482
1483
    public function setSubject($subject): void
1484
    {
1485
        if (is_object($subject) && !is_a($subject, $this->getClass(), true)) {
1486
            $message = <<<'EOT'
1487
You are trying to set entity an instance of "%s",
1488
which is not the one registered with this admin class ("%s").
1489
This is deprecated since 3.5 and will no longer be supported in 4.0.
1490
EOT;
1491
1492
            @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...
1493
                sprintf($message, get_class($subject), $this->getClass()),
1494
                E_USER_DEPRECATED
1495
            ); // NEXT_MAJOR : throw an exception instead
1496
        }
1497
1498
        $this->subject = $subject;
1499
    }
1500
1501
    public function getSubject()
1502
    {
1503
        if (null === $this->subject && $this->request && !$this->hasParentFieldDescription()) {
1504
            $id = $this->request->get($this->getIdParameter());
1505
1506
            if (null !== $id) {
1507
                $this->subject = $this->getObject($id);
1508
            }
1509
        }
1510
1511
        return $this->subject;
1512
    }
1513
1514
    public function hasSubject()
1515
    {
1516
        return (bool) $this->getSubject();
1517
    }
1518
1519
    public function getFormFieldDescriptions()
1520
    {
1521
        $this->buildForm();
1522
1523
        return $this->formFieldDescriptions;
1524
    }
1525
1526
    public function getFormFieldDescription($name)
1527
    {
1528
        return $this->hasFormFieldDescription($name) ? $this->formFieldDescriptions[$name] : null;
1529
    }
1530
1531
    /**
1532
     * Returns true if the admin has a FieldDescription with the given $name.
1533
     *
1534
     * @param string $name
1535
     *
1536
     * @return bool
1537
     */
1538
    public function hasFormFieldDescription($name)
1539
    {
1540
        return array_key_exists($name, $this->formFieldDescriptions) ? true : false;
1541
    }
1542
1543
    public function addFormFieldDescription($name, FieldDescriptionInterface $fieldDescription): void
1544
    {
1545
        $this->formFieldDescriptions[$name] = $fieldDescription;
1546
    }
1547
1548
    /**
1549
     * remove a FieldDescription.
1550
     *
1551
     * @param string $name
1552
     */
1553
    public function removeFormFieldDescription($name): void
1554
    {
1555
        unset($this->formFieldDescriptions[$name]);
1556
    }
1557
1558
    /**
1559
     * build and return the collection of form FieldDescription.
1560
     *
1561
     * @return array collection of form FieldDescription
1562
     */
1563
    public function getShowFieldDescriptions()
1564
    {
1565
        $this->buildShow();
1566
1567
        return $this->showFieldDescriptions;
1568
    }
1569
1570
    /**
1571
     * Returns the form FieldDescription with the given $name.
1572
     *
1573
     * @param string $name
1574
     *
1575
     * @return FieldDescriptionInterface
1576
     */
1577
    public function getShowFieldDescription($name)
1578
    {
1579
        $this->buildShow();
1580
1581
        return $this->hasShowFieldDescription($name) ? $this->showFieldDescriptions[$name] : null;
1582
    }
1583
1584
    public function hasShowFieldDescription($name)
1585
    {
1586
        return array_key_exists($name, $this->showFieldDescriptions);
1587
    }
1588
1589
    public function addShowFieldDescription($name, FieldDescriptionInterface $fieldDescription): void
1590
    {
1591
        $this->showFieldDescriptions[$name] = $fieldDescription;
1592
    }
1593
1594
    public function removeShowFieldDescription($name): void
1595
    {
1596
        unset($this->showFieldDescriptions[$name]);
1597
    }
1598
1599
    public function getListFieldDescriptions()
1600
    {
1601
        $this->buildList();
1602
1603
        return $this->listFieldDescriptions;
1604
    }
1605
1606
    public function getListFieldDescription($name)
1607
    {
1608
        return $this->hasListFieldDescription($name) ? $this->listFieldDescriptions[$name] : null;
1609
    }
1610
1611
    public function hasListFieldDescription($name)
1612
    {
1613
        $this->buildList();
1614
1615
        return array_key_exists($name, $this->listFieldDescriptions) ? true : false;
1616
    }
1617
1618
    public function addListFieldDescription($name, FieldDescriptionInterface $fieldDescription): void
1619
    {
1620
        $this->listFieldDescriptions[$name] = $fieldDescription;
1621
    }
1622
1623
    public function removeListFieldDescription($name): void
1624
    {
1625
        unset($this->listFieldDescriptions[$name]);
1626
    }
1627
1628
    public function getFilterFieldDescription($name)
1629
    {
1630
        return $this->hasFilterFieldDescription($name) ? $this->filterFieldDescriptions[$name] : null;
1631
    }
1632
1633
    public function hasFilterFieldDescription($name)
1634
    {
1635
        return array_key_exists($name, $this->filterFieldDescriptions) ? true : false;
1636
    }
1637
1638
    public function addFilterFieldDescription($name, FieldDescriptionInterface $fieldDescription): void
1639
    {
1640
        $this->filterFieldDescriptions[$name] = $fieldDescription;
1641
    }
1642
1643
    public function removeFilterFieldDescription($name): void
1644
    {
1645
        unset($this->filterFieldDescriptions[$name]);
1646
    }
1647
1648
    public function getFilterFieldDescriptions()
1649
    {
1650
        $this->buildDatagrid();
1651
1652
        return $this->filterFieldDescriptions;
1653
    }
1654
1655
    public function addChild(AdminInterface $child): void
1656
    {
1657
        for ($parentAdmin = $this; null !== $parentAdmin; $parentAdmin = $parentAdmin->getParent()) {
1658
            if ($parentAdmin->getCode() !== $child->getCode()) {
1659
                continue;
1660
            }
1661
1662
            throw new \RuntimeException(sprintf(
1663
                'Circular reference detected! The child admin `%s` is already in the parent tree of the `%s` admin.',
1664
                $child->getCode(), $this->getCode()
1665
            ));
1666
        }
1667
1668
        $this->children[$child->getCode()] = $child;
1669
1670
        $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...
1671
1672
        // NEXT_MAJOR: remove $args and add $field parameter to this function on next Major
1673
1674
        $args = \func_get_args();
1675
1676
        if (isset($args[1])) {
1677
            $child->addParentAssociationMapping($this->getCode(), $args[1]);
1678
        } else {
1679
            @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...
1680
                'Calling "addChild" without second argument is deprecated since 3.x'
1681
                .' and will not be allowed in 4.0.',
1682
                E_USER_DEPRECATED
1683
            );
1684
        }
1685
    }
1686
1687
    public function hasChild($code)
1688
    {
1689
        return isset($this->children[$code]);
1690
    }
1691
1692
    public function getChildren()
1693
    {
1694
        return $this->children;
1695
    }
1696
1697
    public function getChild($code)
1698
    {
1699
        return $this->hasChild($code) ? $this->children[$code] : null;
1700
    }
1701
1702
    public function setParent(AdminInterface $parent): void
1703
    {
1704
        $this->parent = $parent;
1705
    }
1706
1707
    public function getParent()
1708
    {
1709
        return $this->parent;
1710
    }
1711
1712
    final public function getRootAncestor()
1713
    {
1714
        $parent = $this;
1715
1716
        while ($parent->isChild()) {
1717
            $parent = $parent->getParent();
1718
        }
1719
1720
        return $parent;
1721
    }
1722
1723
    final public function getChildDepth()
1724
    {
1725
        $parent = $this;
1726
        $depth = 0;
1727
1728
        while ($parent->isChild()) {
1729
            $parent = $parent->getParent();
1730
            ++$depth;
1731
        }
1732
1733
        return $depth;
1734
    }
1735
1736
    final public function getCurrentLeafChildAdmin()
1737
    {
1738
        $child = $this->getCurrentChildAdmin();
1739
1740
        if (null === $child) {
1741
            return;
1742
        }
1743
1744
        for ($c = $child; null !== $c; $c = $child->getCurrentChildAdmin()) {
1745
            $child = $c;
1746
        }
1747
1748
        return $child;
1749
    }
1750
1751
    public function isChild()
1752
    {
1753
        return $this->parent instanceof AdminInterface;
1754
    }
1755
1756
    /**
1757
     * Returns true if the admin has children, false otherwise.
1758
     *
1759
     * @return bool if the admin has children
1760
     */
1761
    public function hasChildren()
1762
    {
1763
        return count($this->children) > 0;
1764
    }
1765
1766
    public function setUniqid($uniqid): void
1767
    {
1768
        $this->uniqid = $uniqid;
1769
    }
1770
1771
    public function getUniqid()
1772
    {
1773
        if (!$this->uniqid) {
1774
            $this->uniqid = 's'.uniqid();
1775
        }
1776
1777
        return $this->uniqid;
1778
    }
1779
1780
    /**
1781
     * {@inheritdoc}
1782
     */
1783
    public function getClassnameLabel()
1784
    {
1785
        return $this->classnameLabel;
1786
    }
1787
1788
    public function getPersistentParameters()
1789
    {
1790
        $parameters = [];
1791
1792
        foreach ($this->getExtensions() as $extension) {
1793
            $params = $extension->getPersistentParameters($this);
1794
1795
            if (!is_array($params)) {
1796
                throw new \RuntimeException(sprintf('The %s::getPersistentParameters must return an array', get_class($extension)));
1797
            }
1798
1799
            $parameters = array_merge($parameters, $params);
1800
        }
1801
1802
        return $parameters;
1803
    }
1804
1805
    /**
1806
     * {@inheritdoc}
1807
     */
1808
    public function getPersistentParameter($name)
1809
    {
1810
        $parameters = $this->getPersistentParameters();
1811
1812
        return $parameters[$name] ?? null;
1813
    }
1814
1815
    public function setCurrentChild($currentChild): void
1816
    {
1817
        $this->currentChild = $currentChild;
1818
    }
1819
1820
    public function getCurrentChild()
1821
    {
1822
        return $this->currentChild;
1823
    }
1824
1825
    /**
1826
     * Returns the current child admin instance.
1827
     *
1828
     * @return AdminInterface|null the current child admin instance
1829
     */
1830
    public function getCurrentChildAdmin()
1831
    {
1832
        foreach ($this->children as $children) {
1833
            if ($children->getCurrentChild()) {
1834
                return $children;
1835
            }
1836
        }
1837
    }
1838
1839
    public function trans($id, array $parameters = [], $domain = null, $locale = null)
1840
    {
1841
        @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...
1842
            'The '.__METHOD__.' method is deprecated since version 3.9 and will be removed in 4.0.',
1843
            E_USER_DEPRECATED
1844
        );
1845
1846
        $domain = $domain ?: $this->getTranslationDomain();
1847
1848
        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 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...
1849
    }
1850
1851
    /**
1852
     * Translate a message id.
1853
     *
1854
     * NEXT_MAJOR: remove this method
1855
     *
1856
     * @param string      $id
1857
     * @param int         $count
1858
     * @param string|null $domain
1859
     * @param string|null $locale
1860
     *
1861
     * @return string the translated string
1862
     *
1863
     * @deprecated since 3.9, to be removed with 4.0
1864
     */
1865
    public function transChoice($id, $count, array $parameters = [], $domain = null, $locale = null)
1866
    {
1867
        @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...
1868
            'The '.__METHOD__.' method is deprecated since version 3.9 and will be removed in 4.0.',
1869
            E_USER_DEPRECATED
1870
        );
1871
1872
        $domain = $domain ?: $this->getTranslationDomain();
1873
1874
        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 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...
1875
    }
1876
1877
    public function setTranslationDomain($translationDomain): void
1878
    {
1879
        $this->translationDomain = $translationDomain;
1880
    }
1881
1882
    public function getTranslationDomain()
1883
    {
1884
        return $this->translationDomain;
1885
    }
1886
1887
    /**
1888
     * {@inheritdoc}
1889
     *
1890
     * NEXT_MAJOR: remove this method
1891
     *
1892
     * @deprecated since 3.9, to be removed with 4.0
1893
     */
1894
    public function setTranslator(TranslatorInterface $translator): void
1895
    {
1896
        $args = func_get_args();
1897
        if (isset($args[1]) && $args[1]) {
1898
            @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...
1899
                'The '.__METHOD__.' method is deprecated since version 3.9 and will be removed in 4.0.',
1900
                E_USER_DEPRECATED
1901
            );
1902
        }
1903
1904
        $this->translator = $translator;
0 ignored issues
show
Deprecated Code introduced by
The property Sonata\AdminBundle\Admin...tractAdmin::$translator has been deprecated with message: since 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...
1905
    }
1906
1907
    /**
1908
     * {@inheritdoc}
1909
     *
1910
     * NEXT_MAJOR: remove this method
1911
     *
1912
     * @deprecated since 3.9, to be removed with 4.0
1913
     */
1914
    public function getTranslator()
1915
    {
1916
        @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...
1917
            'The '.__METHOD__.' method is deprecated since version 3.9 and will be removed in 4.0.',
1918
            E_USER_DEPRECATED
1919
        );
1920
1921
        return $this->translator;
0 ignored issues
show
Deprecated Code introduced by
The property Sonata\AdminBundle\Admin...tractAdmin::$translator has been deprecated with message: since 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...
1922
    }
1923
1924
    public function getTranslationLabel($label, $context = '', $type = '')
1925
    {
1926
        return $this->getLabelTranslatorStrategy()->getLabel($label, $context, $type);
1927
    }
1928
1929
    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...
1930
    {
1931
        $this->request = $request;
1932
1933
        foreach ($this->getChildren() as $children) {
1934
            $children->setRequest($request);
1935
        }
1936
    }
1937
1938
    public function getRequest()
1939
    {
1940
        if (!$this->request) {
1941
            throw new \RuntimeException('The Request object has not been set');
1942
        }
1943
1944
        return $this->request;
1945
    }
1946
1947
    public function hasRequest()
1948
    {
1949
        return null !== $this->request;
1950
    }
1951
1952
    public function setFormContractor(FormContractorInterface $formBuilder): void
1953
    {
1954
        $this->formContractor = $formBuilder;
1955
    }
1956
1957
    /**
1958
     * @return FormContractorInterface
1959
     */
1960
    public function getFormContractor()
1961
    {
1962
        return $this->formContractor;
1963
    }
1964
1965
    public function setDatagridBuilder(DatagridBuilderInterface $datagridBuilder): void
1966
    {
1967
        $this->datagridBuilder = $datagridBuilder;
1968
    }
1969
1970
    public function getDatagridBuilder()
1971
    {
1972
        return $this->datagridBuilder;
1973
    }
1974
1975
    public function setListBuilder(ListBuilderInterface $listBuilder): void
1976
    {
1977
        $this->listBuilder = $listBuilder;
1978
    }
1979
1980
    public function getListBuilder()
1981
    {
1982
        return $this->listBuilder;
1983
    }
1984
1985
    public function setShowBuilder(ShowBuilderInterface $showBuilder): void
1986
    {
1987
        $this->showBuilder = $showBuilder;
1988
    }
1989
1990
    /**
1991
     * @return ShowBuilderInterface
1992
     */
1993
    public function getShowBuilder()
1994
    {
1995
        return $this->showBuilder;
1996
    }
1997
1998
    public function setConfigurationPool(Pool $configurationPool): void
1999
    {
2000
        $this->configurationPool = $configurationPool;
2001
    }
2002
2003
    /**
2004
     * @return Pool
2005
     */
2006
    public function getConfigurationPool()
2007
    {
2008
        return $this->configurationPool;
2009
    }
2010
2011
    public function setRouteGenerator(RouteGeneratorInterface $routeGenerator): void
2012
    {
2013
        $this->routeGenerator = $routeGenerator;
2014
    }
2015
2016
    /**
2017
     * @return RouteGeneratorInterface
2018
     */
2019
    public function getRouteGenerator()
2020
    {
2021
        return $this->routeGenerator;
2022
    }
2023
2024
    public function getCode()
2025
    {
2026
        return $this->code;
2027
    }
2028
2029
    public function getBaseCodeRoute()
2030
    {
2031
        if ($this->isChild()) {
2032
            return $this->getParent()->getBaseCodeRoute().'|'.$this->getCode();
2033
        }
2034
2035
        return $this->getCode();
2036
    }
2037
2038
    public function getModelManager()
2039
    {
2040
        return $this->modelManager;
2041
    }
2042
2043
    public function setModelManager(ModelManagerInterface $modelManager): void
2044
    {
2045
        $this->modelManager = $modelManager;
2046
    }
2047
2048
    public function getManagerType()
2049
    {
2050
        return $this->managerType;
2051
    }
2052
2053
    /**
2054
     * @param string $type
2055
     */
2056
    public function setManagerType($type): void
2057
    {
2058
        $this->managerType = $type;
2059
    }
2060
2061
    public function getObjectIdentifier()
2062
    {
2063
        return $this->getCode();
2064
    }
2065
2066
    /**
2067
     * Set the roles and permissions per role.
2068
     */
2069
    public function setSecurityInformation(array $information): void
2070
    {
2071
        $this->securityInformation = $information;
2072
    }
2073
2074
    public function getSecurityInformation()
2075
    {
2076
        return $this->securityInformation;
2077
    }
2078
2079
    /**
2080
     * Return the list of permissions the user should have in order to display the admin.
2081
     *
2082
     * @param string $context
2083
     *
2084
     * @return array
2085
     */
2086
    public function getPermissionsShow($context)
2087
    {
2088
        switch ($context) {
2089
            case self::CONTEXT_DASHBOARD:
2090
            case self::CONTEXT_MENU:
2091
            default:
2092
                return ['LIST'];
2093
        }
2094
    }
2095
2096
    public function showIn($context)
2097
    {
2098
        switch ($context) {
2099
            case self::CONTEXT_DASHBOARD:
2100
            case self::CONTEXT_MENU:
2101
            default:
2102
                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...
2103
        }
2104
    }
2105
2106
    public function createObjectSecurity($object): void
2107
    {
2108
        $this->getSecurityHandler()->createObjectSecurity($this, $object);
2109
    }
2110
2111
    public function setSecurityHandler(SecurityHandlerInterface $securityHandler): void
2112
    {
2113
        $this->securityHandler = $securityHandler;
2114
    }
2115
2116
    public function getSecurityHandler()
2117
    {
2118
        return $this->securityHandler;
2119
    }
2120
2121
    public function isGranted($name, $object = null)
2122
    {
2123
        $key = md5(json_encode($name).($object ? '/'.spl_object_hash($object) : ''));
2124
2125
        if (!array_key_exists($key, $this->cacheIsGranted)) {
2126
            $this->cacheIsGranted[$key] = $this->securityHandler->isGranted($this, $name, $object ?: $this);
2127
        }
2128
2129
        return $this->cacheIsGranted[$key];
2130
    }
2131
2132
    public function getUrlsafeIdentifier($entity)
2133
    {
2134
        return $this->getModelManager()->getUrlsafeIdentifier($entity);
2135
    }
2136
2137
    public function getNormalizedIdentifier($entity)
2138
    {
2139
        return $this->getModelManager()->getNormalizedIdentifier($entity);
2140
    }
2141
2142
    public function id($entity)
2143
    {
2144
        return $this->getNormalizedIdentifier($entity);
2145
    }
2146
2147
    public function setValidator($validator): void
2148
    {
2149
        // NEXT_MAJOR: Move ValidatorInterface check to method signature
2150
        if (!$validator instanceof ValidatorInterface) {
2151
            throw new \InvalidArgumentException(
2152
                'Argument 1 must be an instance of Symfony\Component\Validator\Validator\ValidatorInterface'
2153
            );
2154
        }
2155
2156
        $this->validator = $validator;
2157
    }
2158
2159
    public function getValidator()
2160
    {
2161
        return $this->validator;
2162
    }
2163
2164
    public function getShow()
2165
    {
2166
        $this->buildShow();
2167
2168
        return $this->show;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->show; (Sonata\AdminBundle\Admin...ldDescriptionCollection) is incompatible with the return type declared by the interface Sonata\AdminBundle\Admin\AdminInterface::getShow of type array.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
2169
    }
2170
2171
    public function setFormTheme(array $formTheme): void
2172
    {
2173
        $this->formTheme = $formTheme;
2174
    }
2175
2176
    public function getFormTheme()
2177
    {
2178
        return $this->formTheme;
2179
    }
2180
2181
    public function setFilterTheme(array $filterTheme): void
2182
    {
2183
        $this->filterTheme = $filterTheme;
2184
    }
2185
2186
    public function getFilterTheme()
2187
    {
2188
        return $this->filterTheme;
2189
    }
2190
2191
    public function addExtension(AdminExtensionInterface $extension): void
2192
    {
2193
        $this->extensions[] = $extension;
2194
    }
2195
2196
    public function getExtensions()
2197
    {
2198
        return $this->extensions;
2199
    }
2200
2201
    public function setMenuFactory(MenuFactoryInterface $menuFactory): void
2202
    {
2203
        $this->menuFactory = $menuFactory;
2204
    }
2205
2206
    public function getMenuFactory()
2207
    {
2208
        return $this->menuFactory;
2209
    }
2210
2211
    public function setRouteBuilder(RouteBuilderInterface $routeBuilder): void
2212
    {
2213
        $this->routeBuilder = $routeBuilder;
2214
    }
2215
2216
    public function getRouteBuilder()
2217
    {
2218
        return $this->routeBuilder;
2219
    }
2220
2221
    public function toString($object)
2222
    {
2223
        if (!is_object($object)) {
2224
            return '';
2225
        }
2226
2227
        if (method_exists($object, '__toString') && null !== $object->__toString()) {
2228
            return (string) $object;
2229
        }
2230
2231
        return sprintf('%s:%s', ClassUtils::getClass($object), spl_object_hash($object));
2232
    }
2233
2234
    public function setLabelTranslatorStrategy(LabelTranslatorStrategyInterface $labelTranslatorStrategy): void
2235
    {
2236
        $this->labelTranslatorStrategy = $labelTranslatorStrategy;
2237
    }
2238
2239
    public function getLabelTranslatorStrategy()
2240
    {
2241
        return $this->labelTranslatorStrategy;
2242
    }
2243
2244
    public function supportsPreviewMode()
2245
    {
2246
        return $this->supportsPreviewMode;
2247
    }
2248
2249
    /**
2250
     * Set custom per page options.
2251
     */
2252
    public function setPerPageOptions(array $options): void
2253
    {
2254
        $this->perPageOptions = $options;
2255
    }
2256
2257
    /**
2258
     * Returns predefined per page options.
2259
     *
2260
     * @return array
2261
     */
2262
    public function getPerPageOptions()
2263
    {
2264
        return $this->perPageOptions;
2265
    }
2266
2267
    /**
2268
     * Set pager type.
2269
     *
2270
     * @param string $pagerType
2271
     */
2272
    public function setPagerType($pagerType): void
2273
    {
2274
        $this->pagerType = $pagerType;
2275
    }
2276
2277
    /**
2278
     * Get pager type.
2279
     *
2280
     * @return string
2281
     */
2282
    public function getPagerType()
2283
    {
2284
        return $this->pagerType;
2285
    }
2286
2287
    /**
2288
     * Returns true if the per page value is allowed, false otherwise.
2289
     *
2290
     * @param int $perPage
2291
     *
2292
     * @return bool
2293
     */
2294
    public function determinedPerPageValue($perPage)
2295
    {
2296
        return in_array($perPage, $this->perPageOptions);
2297
    }
2298
2299
    public function isAclEnabled()
2300
    {
2301
        return $this->getSecurityHandler() instanceof AclSecurityHandlerInterface;
2302
    }
2303
2304
    public function getObjectMetadata($object)
2305
    {
2306
        return new Metadata($this->toString($object));
2307
    }
2308
2309
    public function getListModes()
2310
    {
2311
        return $this->listModes;
2312
    }
2313
2314
    public function setListMode($mode): void
2315
    {
2316
        if (!$this->hasRequest()) {
2317
            throw new \RuntimeException(sprintf('No request attached to the current admin: %s', $this->getCode()));
2318
        }
2319
2320
        $this->getRequest()->getSession()->set(sprintf('%s.list_mode', $this->getCode()), $mode);
2321
    }
2322
2323
    public function getListMode()
2324
    {
2325
        if (!$this->hasRequest()) {
2326
            return 'list';
2327
        }
2328
2329
        return $this->getRequest()->getSession()->get(sprintf('%s.list_mode', $this->getCode()), 'list');
2330
    }
2331
2332
    public function getAccessMapping()
2333
    {
2334
        return $this->accessMapping;
2335
    }
2336
2337
    public function checkAccess($action, $object = null): void
2338
    {
2339
        $access = $this->getAccess();
2340
2341
        if (!array_key_exists($action, $access)) {
2342
            throw new \InvalidArgumentException(sprintf(
2343
                'Action "%s" could not be found in access mapping.'
2344
                .' Please make sure your action is defined into your admin class accessMapping property.',
2345
                $action
2346
            ));
2347
        }
2348
2349
        if (!is_array($access[$action])) {
2350
            $access[$action] = [$access[$action]];
2351
        }
2352
2353
        foreach ($access[$action] as $role) {
2354
            if (false === $this->isGranted($role, $object)) {
2355
                throw new AccessDeniedException(sprintf('Access Denied to the action %s and role %s', $action, $role));
2356
            }
2357
        }
2358
    }
2359
2360
    /**
2361
     * {@inheritdoc}
2362
     */
2363
    public function hasAccess($action, $object = null)
2364
    {
2365
        $access = $this->getAccess();
2366
2367
        if (!array_key_exists($action, $access)) {
2368
            return false;
2369
        }
2370
2371
        if (!is_array($access[$action])) {
2372
            $access[$action] = [$access[$action]];
2373
        }
2374
2375
        foreach ($access[$action] as $role) {
2376
            if (false === $this->isGranted($role, $object)) {
2377
                return false;
2378
            }
2379
        }
2380
2381
        return true;
2382
    }
2383
2384
    final public function getActionButtons($action, $object = null)
2385
    {
2386
        $buttonList = [];
2387
2388
        if (in_array($action, ['tree', 'show', 'edit', 'delete', 'list', 'batch'])
2389
            && $this->hasAccess('create')
2390
            && $this->hasRoute('create')
2391
        ) {
2392
            $buttonList['create'] = [
2393
                'template' => $this->getTemplateRegistry()->getTemplate('button_create'),
2394
            ];
2395
        }
2396
2397
        if (in_array($action, ['show', 'delete', 'acl', 'history'])
2398
            && $this->canAccessObject('edit', $object)
2399
            && $this->hasRoute('edit')
2400
        ) {
2401
            $buttonList['edit'] = [
2402
                'template' => $this->getTemplateRegistry()->getTemplate('button_edit'),
2403
            ];
2404
        }
2405
2406
        if (in_array($action, ['show', 'edit', 'acl'])
2407
            && $this->canAccessObject('history', $object)
2408
            && $this->hasRoute('history')
2409
        ) {
2410
            $buttonList['history'] = [
2411
                'template' => $this->getTemplateRegistry()->getTemplate('button_history'),
2412
            ];
2413
        }
2414
2415
        if (in_array($action, ['edit', 'history'])
2416
            && $this->isAclEnabled()
2417
            && $this->canAccessObject('acl', $object)
2418
            && $this->hasRoute('acl')
2419
        ) {
2420
            $buttonList['acl'] = [
2421
                'template' => $this->getTemplateRegistry()->getTemplate('button_acl'),
2422
            ];
2423
        }
2424
2425
        if (in_array($action, ['edit', 'history', 'acl'])
2426
            && $this->canAccessObject('show', $object)
2427
            && count($this->getShow()) > 0
2428
            && $this->hasRoute('show')
2429
        ) {
2430
            $buttonList['show'] = [
2431
                'template' => $this->getTemplateRegistry()->getTemplate('button_show'),
2432
            ];
2433
        }
2434
2435
        if (in_array($action, ['show', 'edit', 'delete', 'acl', 'batch'])
2436
            && $this->hasAccess('list')
2437
            && $this->hasRoute('list')
2438
        ) {
2439
            $buttonList['list'] = [
2440
                'template' => $this->getTemplateRegistry()->getTemplate('button_list'),
2441
            ];
2442
        }
2443
2444
        $buttonList = $this->configureActionButtons($buttonList, $action, $object);
2445
2446
        foreach ($this->getExtensions() as $extension) {
2447
            $buttonList = $extension->configureActionButtons($this, $buttonList, $action, $object);
2448
        }
2449
2450
        return $buttonList;
2451
    }
2452
2453
    /**
2454
     * {@inheritdoc}
2455
     */
2456
    public function getDashboardActions()
2457
    {
2458
        $actions = [];
2459
2460
        if ($this->hasRoute('create') && $this->hasAccess('create')) {
2461
            $actions['create'] = [
2462
                'label' => 'link_add',
2463
                'translation_domain' => 'SonataAdminBundle',
2464
                // NEXT_MAJOR: Remove this line and use commented line below it instead
2465
                'template' => $this->getTemplate('action_create'),
0 ignored issues
show
Deprecated Code introduced by
The method Sonata\AdminBundle\Admin...actAdmin::getTemplate() has been deprecated with message: since 3.34, will be dropped in 4.0. Use TemplateRegistry services instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
2466
                // 'template' => $this->getTemplateRegistry()->getTemplate('action_create'),
2467
                'url' => $this->generateUrl('create'),
2468
                'icon' => 'plus-circle',
2469
            ];
2470
        }
2471
2472
        if ($this->hasRoute('list') && $this->hasAccess('list')) {
2473
            $actions['list'] = [
2474
                'label' => 'link_list',
2475
                'translation_domain' => 'SonataAdminBundle',
2476
                'url' => $this->generateUrl('list'),
2477
                'icon' => 'list',
2478
            ];
2479
        }
2480
2481
        return $actions;
2482
    }
2483
2484
    /**
2485
     * {@inheritdoc}
2486
     */
2487
    final public function showMosaicButton($isShown): void
2488
    {
2489
        if ($isShown) {
2490
            $this->listModes['mosaic'] = ['class' => self::MOSAIC_ICON_CLASS];
2491
        } else {
2492
            unset($this->listModes['mosaic']);
2493
        }
2494
    }
2495
2496
    /**
2497
     * {@inheritdoc}
2498
     */
2499
    final public function getSearchResultLink($object)
2500
    {
2501
        foreach ($this->searchResultActions as $action) {
2502
            if ($this->hasRoute($action) && $this->hasAccess($action, $object)) {
2503
                return $this->generateObjectUrl($action, $object);
2504
            }
2505
        }
2506
    }
2507
2508
    /**
2509
     * Checks if a filter type is set to a default value.
2510
     *
2511
     * @param string $name
2512
     *
2513
     * @return bool
2514
     */
2515
    final public function isDefaultFilter($name)
2516
    {
2517
        $filter = $this->getFilterParameters();
2518
        $default = $this->getDefaultFilterValues();
2519
2520
        if (!array_key_exists($name, $filter) || !array_key_exists($name, $default)) {
2521
            return false;
2522
        }
2523
2524
        return $filter[$name] == $default[$name];
2525
    }
2526
2527
    /**
2528
     * Check object existence and access, without throw Exception.
2529
     *
2530
     * @param string $action
2531
     * @param object $object
2532
     *
2533
     * @return bool
2534
     */
2535
    public function canAccessObject($action, $object)
2536
    {
2537
        return $object && $this->id($object) && $this->hasAccess($action, $object);
2538
    }
2539
2540
    /**
2541
     * Hook to run after initilization.
2542
     */
2543
    protected function configure(): void
2544
    {
2545
    }
2546
2547
    /**
2548
     * urlize the given word.
2549
     *
2550
     * @param string $word
2551
     * @param string $sep  the separator
2552
     *
2553
     * @return string
2554
     */
2555
    final protected function urlize($word, $sep = '_')
2556
    {
2557
        return strtolower(preg_replace('/[^a-z0-9_]/i', $sep.'$1', $word));
2558
    }
2559
2560
    final protected function getTemplateRegistry(): MutableTemplateRegistryInterface
2561
    {
2562
        return $this->templateRegistry;
2563
    }
2564
2565
    /**
2566
     * Returns a list of default filters.
2567
     *
2568
     * @return array
2569
     */
2570
    final protected function getDefaultFilterValues()
2571
    {
2572
        $defaultFilterValues = [];
2573
2574
        $this->configureDefaultFilterValues($defaultFilterValues);
2575
2576
        foreach ($this->getExtensions() as $extension) {
2577
            // NEXT_MAJOR: remove method check in next major release
2578
            if (method_exists($extension, 'configureDefaultFilterValues')) {
2579
                $extension->configureDefaultFilterValues($this, $defaultFilterValues);
2580
            }
2581
        }
2582
2583
        return $defaultFilterValues;
2584
    }
2585
2586
    protected function configureFormFields(FormMapper $form): void
2587
    {
2588
    }
2589
2590
    protected function configureListFields(ListMapper $list): void
2591
    {
2592
    }
2593
2594
    protected function configureDatagridFilters(DatagridMapper $filter): void
2595
    {
2596
    }
2597
2598
    protected function configureShowFields(ShowMapper $show): void
2599
    {
2600
    }
2601
2602
    protected function configureRoutes(RouteCollection $collection): void
2603
    {
2604
    }
2605
2606
    protected function configureActionButtons($buttonList, $action, $object = null)
0 ignored issues
show
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 $object 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...
2607
    {
2608
        return $buttonList;
2609
    }
2610
2611
    /**
2612
     * Allows you to customize batch actions.
2613
     *
2614
     * @param array $actions List of actions
2615
     *
2616
     * @return array
2617
     */
2618
    protected function configureBatchActions($actions)
2619
    {
2620
        return $actions;
2621
    }
2622
2623
    /**
2624
     * NEXT_MAJOR: remove this method.
2625
     *
2626
     * @return mixed
2627
     *
2628
     * @deprecated Use configureTabMenu instead
2629
     */
2630
    protected function configureSideMenu(MenuItemInterface $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...
2631
    {
2632
    }
2633
2634
    /**
2635
     * Configures the tab menu in your admin.
2636
     *
2637
     * @param string $action
2638
     *
2639
     * @return mixed
2640
     */
2641
    protected function configureTabMenu(MenuItemInterface $menu, $action, AdminInterface $childAdmin = null)
2642
    {
2643
        // Use configureSideMenu not to mess with previous overrides
2644
        // TODO remove once deprecation period is over
2645
        $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...
2646
    }
2647
2648
    /**
2649
     * build the view FieldDescription array.
2650
     */
2651
    protected function buildShow(): void
2652
    {
2653
        if ($this->show) {
2654
            return;
2655
        }
2656
2657
        $this->show = new FieldDescriptionCollection();
2658
        $mapper = new ShowMapper($this->showBuilder, $this->show, $this);
2659
2660
        $this->configureShowFields($mapper);
2661
2662
        foreach ($this->getExtensions() as $extension) {
2663
            $extension->configureShowFields($mapper);
2664
        }
2665
    }
2666
2667
    /**
2668
     * build the list FieldDescription array.
2669
     */
2670
    protected function buildList(): void
2671
    {
2672
        if ($this->list) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->list of type array 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...
2673
            return;
2674
        }
2675
2676
        $this->list = $this->getListBuilder()->getBaseList();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getListBuilder()->getBaseList() of type object<Sonata\AdminBundl...dDescriptionCollection> is incompatible with the declared type array of property $list.

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...
2677
2678
        $mapper = new ListMapper($this->getListBuilder(), $this->list, $this);
2679
2680
        if (count($this->getBatchActions()) > 0) {
2681
            $fieldDescription = $this->getModelManager()->getNewFieldDescriptionInstance(
2682
                $this->getClass(),
2683
                'batch',
2684
                [
2685
                    'label' => 'batch',
2686
                    'code' => '_batch',
2687
                    'sortable' => false,
2688
                    'virtual_field' => true,
2689
                ]
2690
            );
2691
2692
            $fieldDescription->setAdmin($this);
2693
            // NEXT_MAJOR: Remove this line and use commented line below it instead
2694
            $fieldDescription->setTemplate($this->getTemplate('batch'));
0 ignored issues
show
Deprecated Code introduced by
The method Sonata\AdminBundle\Admin...actAdmin::getTemplate() has been deprecated with message: since 3.34, will be dropped in 4.0. Use TemplateRegistry services instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
2695
            // $fieldDescription->setTemplate($this->getTemplateRegistry()->getTemplate('batch'));
2696
2697
            $mapper->add($fieldDescription, 'batch');
0 ignored issues
show
Documentation introduced by
$fieldDescription is of type object<Sonata\AdminBundl...ldDescriptionInterface>, 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...
2698
        }
2699
2700
        $this->configureListFields($mapper);
2701
2702
        foreach ($this->getExtensions() as $extension) {
2703
            $extension->configureListFields($mapper);
2704
        }
2705
2706
        if ($this->hasRequest() && $this->getRequest()->isXmlHttpRequest()) {
2707
            $fieldDescription = $this->getModelManager()->getNewFieldDescriptionInstance(
2708
                $this->getClass(),
2709
                'select',
2710
                [
2711
                    'label' => false,
2712
                    'code' => '_select',
2713
                    'sortable' => false,
2714
                    'virtual_field' => false,
2715
                ]
2716
            );
2717
2718
            $fieldDescription->setAdmin($this);
2719
            // NEXT_MAJOR: Remove this line and use commented line below it instead
2720
            $fieldDescription->setTemplate($this->getTemplate('select'));
0 ignored issues
show
Deprecated Code introduced by
The method Sonata\AdminBundle\Admin...actAdmin::getTemplate() has been deprecated with message: since 3.34, will be dropped in 4.0. Use TemplateRegistry services instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
2721
            // $fieldDescription->setTemplate($this->getTemplateRegistry()->getTemplate('select'));
2722
2723
            $mapper->add($fieldDescription, 'select');
0 ignored issues
show
Documentation introduced by
$fieldDescription is of type object<Sonata\AdminBundl...ldDescriptionInterface>, 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...
2724
        }
2725
    }
2726
2727
    /**
2728
     * Build the form FieldDescription collection.
2729
     */
2730
    protected function buildForm(): void
2731
    {
2732
        if ($this->form) {
2733
            return;
2734
        }
2735
2736
        // append parent object if any
2737
        // todo : clean the way the Admin class can retrieve set the object
2738
        if ($this->isChild() && $this->getParentAssociationMapping()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getParentAssociationMapping() of type null|string 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...
2739
            $parent = $this->getParent()->getObject($this->request->get($this->getParent()->getIdParameter()));
2740
2741
            $propertyAccessor = $this->getConfigurationPool()->getPropertyAccessor();
2742
            $propertyPath = new PropertyPath($this->getParentAssociationMapping());
2743
2744
            $object = $this->getSubject();
2745
2746
            $value = $propertyAccessor->getValue($object, $propertyPath);
2747
2748
            if (is_array($value) || ($value instanceof \Traversable && $value instanceof \ArrayAccess)) {
2749
                $value[] = $parent;
2750
                $propertyAccessor->setValue($object, $propertyPath, $value);
2751
            } else {
2752
                $propertyAccessor->setValue($object, $propertyPath, $parent);
2753
            }
2754
        }
2755
2756
        $formBuilder = $this->getFormBuilder();
2757
        $formBuilder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event): void {
2758
            $this->preValidate($event->getData());
2759
        }, 100);
2760
2761
        $this->form = $formBuilder->getForm();
2762
    }
2763
2764
    /**
2765
     * Gets the subclass corresponding to the given name.
2766
     *
2767
     * @param string $name The name of the sub class
2768
     *
2769
     * @return string the subclass
2770
     */
2771
    protected function getSubClass($name)
2772
    {
2773
        if ($this->hasSubClass($name)) {
2774
            return $this->subClasses[$name];
2775
        }
2776
2777
        throw new \RuntimeException(sprintf(
2778
            'Unable to find the subclass `%s` for admin `%s`',
2779
            $name,
2780
            get_class($this)
2781
        ));
2782
    }
2783
2784
    /**
2785
     * Attach the inline validator to the model metadata, this must be done once per admin.
2786
     */
2787
    protected function attachInlineValidator(): void
2788
    {
2789
        $admin = $this;
2790
2791
        // add the custom inline validation option
2792
        $metadata = $this->validator->getMetadataFor($this->getClass());
2793
2794
        $metadata->addConstraint(new InlineConstraint([
2795
            'service' => $this,
2796
            'method' => function (ErrorElement $errorElement, $object) use ($admin): void {
2797
                /* @var \Sonata\AdminBundle\Admin\AdminInterface $admin */
2798
2799
                // This avoid the main validation to be cascaded to children
2800
                // The problem occurs when a model Page has a collection of Page as property
2801
                if ($admin->hasSubject() && spl_object_hash($object) !== spl_object_hash($admin->getSubject())) {
2802
                    return;
2803
                }
2804
2805
                $admin->validate($errorElement, $object);
2806
2807
                foreach ($admin->getExtensions() as $extension) {
2808
                    $extension->validate($admin, $errorElement, $object);
2809
                }
2810
            },
2811
            'serializingWarning' => true,
2812
        ]));
2813
    }
2814
2815
    /**
2816
     * Predefine per page options.
2817
     */
2818
    protected function predefinePerPageOptions(): void
2819
    {
2820
        array_unshift($this->perPageOptions, $this->maxPerPage);
2821
        $this->perPageOptions = array_unique($this->perPageOptions);
2822
        sort($this->perPageOptions);
2823
    }
2824
2825
    /**
2826
     * Return list routes with permissions name.
2827
     *
2828
     * @return array
2829
     */
2830
    protected function getAccess()
2831
    {
2832
        $access = array_merge([
2833
            'acl' => 'MASTER',
2834
            'export' => 'EXPORT',
2835
            'historyCompareRevisions' => 'EDIT',
2836
            'historyViewRevision' => 'EDIT',
2837
            'history' => 'EDIT',
2838
            'edit' => 'EDIT',
2839
            'show' => 'VIEW',
2840
            'create' => 'CREATE',
2841
            'delete' => 'DELETE',
2842
            'batchDelete' => 'DELETE',
2843
            'list' => 'LIST',
2844
        ], $this->getAccessMapping());
2845
2846
        foreach ($this->extensions as $extension) {
2847
            $access = array_merge($access, $extension->getAccessMapping($this));
2848
        }
2849
2850
        return $access;
2851
    }
2852
2853
    /**
2854
     * Returns a list of default filters.
2855
     */
2856
    protected function configureDefaultFilterValues(array &$filterValues): void
2857
    {
2858
    }
2859
2860
    /**
2861
     * {@inheritdoc}
2862
     */
2863
    private function buildDatagrid(): void
2864
    {
2865
        if ($this->datagrid) {
2866
            return;
2867
        }
2868
2869
        $filterParameters = $this->getFilterParameters();
2870
2871
        // transform _sort_by from a string to a FieldDescriptionInterface for the datagrid.
2872
        if (isset($filterParameters['_sort_by']) && is_string($filterParameters['_sort_by'])) {
2873
            if ($this->hasListFieldDescription($filterParameters['_sort_by'])) {
2874
                $filterParameters['_sort_by'] = $this->getListFieldDescription($filterParameters['_sort_by']);
2875
            } else {
2876
                $filterParameters['_sort_by'] = $this->getModelManager()->getNewFieldDescriptionInstance(
2877
                    $this->getClass(),
2878
                    $filterParameters['_sort_by'],
2879
                    []
2880
                );
2881
2882
                $this->getListBuilder()->buildField(null, $filterParameters['_sort_by'], $this);
2883
            }
2884
        }
2885
2886
        // initialize the datagrid
2887
        $this->datagrid = $this->getDatagridBuilder()->getBaseDatagrid($this, $filterParameters);
2888
2889
        $this->datagrid->getPager()->setMaxPageLinks($this->maxPageLinks);
2890
2891
        $mapper = new DatagridMapper($this->getDatagridBuilder(), $this->datagrid, $this);
2892
2893
        // build the datagrid filter
2894
        $this->configureDatagridFilters($mapper);
2895
2896
        // ok, try to limit to add parent filter
2897
        if ($this->isChild() && $this->getParentAssociationMapping() && !$mapper->has($this->getParentAssociationMapping())) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getParentAssociationMapping() of type null|string 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...
2898
            $mapper->add($this->getParentAssociationMapping(), null, [
2899
                'show_filter' => false,
2900
                'label' => false,
2901
                'field_type' => ModelHiddenType::class,
2902
                'field_options' => [
2903
                    'model_manager' => $this->getModelManager(),
2904
                ],
2905
                'operator_type' => HiddenType::class,
2906
            ], null, null, [
2907
                'admin_code' => $this->getParent()->getCode(),
2908
            ]);
2909
        }
2910
2911
        foreach ($this->getExtensions() as $extension) {
2912
            $extension->configureDatagridFilters($mapper);
2913
        }
2914
    }
2915
2916
    /**
2917
     * Build all the related urls to the current admin.
2918
     */
2919
    private function buildRoutes(): void
2920
    {
2921
        if ($this->loaded['routes']) {
2922
            return;
2923
        }
2924
2925
        $this->loaded['routes'] = true;
2926
2927
        $this->routes = new RouteCollection(
2928
            $this->getBaseCodeRoute(),
2929
            $this->getBaseRouteName(),
2930
            $this->getBaseRoutePattern(),
2931
            $this->getBaseControllerName()
2932
        );
2933
2934
        $this->routeBuilder->build($this, $this->routes);
2935
2936
        $this->configureRoutes($this->routes);
2937
2938
        foreach ($this->getExtensions() as $extension) {
2939
            $extension->configureRoutes($this, $this->routes);
2940
        }
2941
    }
2942
}
2943