Completed
Push — master ( 46c3a0...a94e83 )
by Grégoire
13s queued 11s
created

src/Admin/AbstractAdmin.php (6 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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\Object\Metadata;
35
use Sonata\AdminBundle\Route\RouteCollection;
36
use Sonata\AdminBundle\Route\RouteGeneratorInterface;
37
use Sonata\AdminBundle\Security\Handler\AclSecurityHandlerInterface;
38
use Sonata\AdminBundle\Security\Handler\SecurityHandlerInterface;
39
use Sonata\AdminBundle\Show\ShowMapper;
40
use Sonata\AdminBundle\Templating\MutableTemplateRegistryInterface;
41
use Sonata\AdminBundle\Translator\LabelTranslatorStrategyInterface;
42
use Sonata\Form\Validator\Constraints\InlineConstraint;
43
use Sonata\Form\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|null
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 Request|null
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 DatagridInterface|null
342
     */
343
    protected $datagrid;
344
345
    /**
346
     * The router instance.
347
     *
348
     * @var RouteGeneratorInterface|null
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<string, bool>
381
     */
382
    protected $loaded = [
383
        'view_fields' => false,
384
        'view_groups' => false,
385
        'routes' => false,
386
        'tab_menu' => false,
387
    ];
388
389
    /**
390
     * @var string[]
391
     */
392
    protected $formTheme = [];
393
394
    /**
395
     * @var string[]
396
     */
397
    protected $filterTheme = [];
398
399
    /**
400
     * @var array<string, string>
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<string, string>
473
     */
474
    private $subClasses = [];
475
476
    /**
477
     * The list collection.
478
     *
479
     * @var FieldDescriptionCollection
480
     */
481
    private $list;
482
483
    /**
484
     * @var FieldDescriptionCollection|null
485
     */
486
    private $show;
487
488
    /**
489
     * @var Form|null
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(): array
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
            /* NEXT_MAJOR: remove cast to string, null is not supposed to be
631
            supported but was documented as such */
632
            $this->classnameLabel = substr(
633
                (string) $this->getClass(),
634
                strrpos((string) $this->getClass(), '\\') + 1
635
            );
636
        }
637
638
        $this->configure();
639
    }
640
641
    public function update($object)
642
    {
643
        $this->preUpdate($object);
644
        foreach ($this->extensions as $extension) {
645
            $extension->preUpdate($this, $object);
646
        }
647
648
        $result = $this->getModelManager()->update($object);
649
        // BC compatibility
650
        if (null !== $result) {
651
            $object = $result;
652
        }
653
654
        $this->postUpdate($object);
655
        foreach ($this->extensions as $extension) {
656
            $extension->postUpdate($this, $object);
657
        }
658
659
        return $object;
660
    }
661
662
    public function create($object)
663
    {
664
        $this->prePersist($object);
665
        foreach ($this->extensions as $extension) {
666
            $extension->prePersist($this, $object);
667
        }
668
669
        $result = $this->getModelManager()->create($object);
670
        // BC compatibility
671
        if (null !== $result) {
672
            $object = $result;
673
        }
674
675
        $this->postPersist($object);
676
        foreach ($this->extensions as $extension) {
677
            $extension->postPersist($this, $object);
678
        }
679
680
        $this->createObjectSecurity($object);
681
682
        return $object;
683
    }
684
685
    public function delete($object): void
686
    {
687
        $this->preRemove($object);
688
        foreach ($this->extensions as $extension) {
689
            $extension->preRemove($this, $object);
690
        }
691
692
        $this->getSecurityHandler()->deleteObjectSecurity($this, $object);
693
        $this->getModelManager()->delete($object);
694
695
        $this->postRemove($object);
696
        foreach ($this->extensions as $extension) {
697
            $extension->postRemove($this, $object);
698
        }
699
    }
700
701
    /**
702
     * @param object $object
703
     */
704
    public function preValidate($object): void
705
    {
706
    }
707
708
    public function preUpdate($object): void
709
    {
710
    }
711
712
    public function postUpdate($object): void
713
    {
714
    }
715
716
    public function prePersist($object): void
717
    {
718
    }
719
720
    public function postPersist($object): void
721
    {
722
    }
723
724
    public function preRemove($object): void
725
    {
726
    }
727
728
    public function postRemove($object): void
729
    {
730
    }
731
732
    public function preBatchAction($actionName, ProxyQueryInterface $query, array &$idx, $allElements): void
733
    {
734
    }
735
736
    public function getFilterParameters()
737
    {
738
        $parameters = [];
739
740
        // build the values array
741
        if ($this->hasRequest()) {
742
            $filters = $this->request->query->get('filter', []);
743
            if (isset($filters['_page'])) {
744
                $filters['_page'] = (int) $filters['_page'];
745
            }
746
            if (isset($filters['_per_page'])) {
747
                $filters['_per_page'] = (int) $filters['_per_page'];
748
            }
749
750
            // if filter persistence is configured
751
            // NEXT_MAJOR: remove `$this->persistFilters !== false` from the condition
752
            if (false !== $this->persistFilters && null !== $this->filterPersister) {
753
                // if reset filters is asked, remove from storage
754
                if ('reset' === $this->request->query->get('filters')) {
755
                    $this->filterPersister->reset($this->getCode());
756
                }
757
758
                // if no filters, fetch from storage
759
                // otherwise save to storage
760
                if (empty($filters)) {
761
                    $filters = $this->filterPersister->get($this->getCode());
762
                } else {
763
                    $this->filterPersister->set($this->getCode(), $filters);
764
                }
765
            }
766
767
            $parameters = array_merge(
768
                $this->getModelManager()->getDefaultSortValues($this->getClass()),
769
                $this->datagridValues,
770
                $this->getDefaultFilterValues(),
771
                $filters
772
            );
773
774
            if (!$this->determinedPerPageValue($parameters['_per_page'])) {
775
                $parameters['_per_page'] = $this->maxPerPage;
776
            }
777
778
            // always force the parent value
779
            if ($this->isChild() && $this->getParentAssociationMapping()) {
780
                $name = str_replace('.', '__', $this->getParentAssociationMapping());
781
                $parameters[$name] = ['value' => $this->request->get($this->getParent()->getIdParameter())];
782
            }
783
        }
784
785
        return $parameters;
786
    }
787
788
    /**
789
     * Returns the name of the parent related field, so the field can be use to set the default
790
     * value (ie the parent object) or to filter the object.
791
     *
792
     * @throws \InvalidArgumentException
793
     *
794
     * @return string|null
795
     */
796
    public function getParentAssociationMapping()
797
    {
798
        // NEXT_MAJOR: remove array check
799
        if (\is_array($this->parentAssociationMapping) && $this->getParent()) {
800
            $parent = $this->getParent()->getCode();
801
802
            if (\array_key_exists($parent, $this->parentAssociationMapping)) {
803
                return $this->parentAssociationMapping[$parent];
804
            }
805
806
            throw new \InvalidArgumentException(sprintf(
807
                "There's no association between %s and %s.",
808
                $this->getCode(),
809
                $this->getParent()->getCode()
810
            ));
811
        }
812
813
        // NEXT_MAJOR: remove this line
814
        return $this->parentAssociationMapping;
815
    }
816
817
    /**
818
     * @param string $code
819
     * @param string $value
820
     */
821
    final public function addParentAssociationMapping($code, $value): void
822
    {
823
        $this->parentAssociationMapping[$code] = $value;
824
    }
825
826
    /**
827
     * Returns the baseRoutePattern used to generate the routing information.
828
     *
829
     * @throws \RuntimeException
830
     *
831
     * @return string the baseRoutePattern used to generate the routing information
832
     */
833
    public function getBaseRoutePattern()
834
    {
835
        if (null !== $this->cachedBaseRoutePattern) {
836
            return $this->cachedBaseRoutePattern;
837
        }
838
839
        if ($this->isChild()) { // the admin class is a child, prefix it with the parent route pattern
840
            $baseRoutePattern = $this->baseRoutePattern;
841
            if (!$this->baseRoutePattern) {
842
                preg_match(self::CLASS_REGEX, $this->class, $matches);
843
844
                if (!$matches) {
845
                    throw new \RuntimeException(sprintf('Please define a default `baseRoutePattern` value for the admin class `%s`', static::class));
846
                }
847
                $baseRoutePattern = $this->urlize($matches[5], '-');
848
            }
849
850
            $this->cachedBaseRoutePattern = sprintf(
851
                '%s/%s/%s',
852
                $this->getParent()->getBaseRoutePattern(),
853
                $this->getParent()->getRouterIdParameter(),
854
                $baseRoutePattern
855
            );
856
        } elseif ($this->baseRoutePattern) {
857
            $this->cachedBaseRoutePattern = $this->baseRoutePattern;
858
        } else {
859
            preg_match(self::CLASS_REGEX, $this->class, $matches);
860
861
            if (!$matches) {
862
                throw new \RuntimeException(sprintf('Please define a default `baseRoutePattern` value for the admin class `%s`', static::class));
863
            }
864
865
            $this->cachedBaseRoutePattern = sprintf(
866
                '/%s%s/%s',
867
                empty($matches[1]) ? '' : $this->urlize($matches[1], '-').'/',
868
                $this->urlize($matches[3], '-'),
869
                $this->urlize($matches[5], '-')
870
            );
871
        }
872
873
        return $this->cachedBaseRoutePattern;
874
    }
875
876
    /**
877
     * Returns the baseRouteName used to generate the routing information.
878
     *
879
     * @throws \RuntimeException
880
     *
881
     * @return string the baseRouteName used to generate the routing information
882
     */
883
    public function getBaseRouteName()
884
    {
885
        if (null !== $this->cachedBaseRouteName) {
886
            return $this->cachedBaseRouteName;
887
        }
888
889
        if ($this->isChild()) { // the admin class is a child, prefix it with the parent route name
890
            $baseRouteName = $this->baseRouteName;
891
            if (!$this->baseRouteName) {
892
                preg_match(self::CLASS_REGEX, $this->class, $matches);
893
894
                if (!$matches) {
895
                    throw new \RuntimeException(sprintf('Cannot automatically determine base route name, please define a default `baseRouteName` value for the admin class `%s`', static::class));
896
                }
897
                $baseRouteName = $this->urlize($matches[5]);
898
            }
899
900
            $this->cachedBaseRouteName = sprintf(
901
                '%s_%s',
902
                $this->getParent()->getBaseRouteName(),
903
                $baseRouteName
904
            );
905
        } elseif ($this->baseRouteName) {
906
            $this->cachedBaseRouteName = $this->baseRouteName;
907
        } else {
908
            preg_match(self::CLASS_REGEX, $this->class, $matches);
909
910
            if (!$matches) {
911
                throw new \RuntimeException(sprintf('Cannot automatically determine base route name, please define a default `baseRouteName` value for the admin class `%s`', static::class));
912
            }
913
914
            $this->cachedBaseRouteName = sprintf('admin_%s%s_%s',
915
                empty($matches[1]) ? '' : $this->urlize($matches[1]).'_',
916
                $this->urlize($matches[3]),
917
                $this->urlize($matches[5])
918
            );
919
        }
920
921
        return $this->cachedBaseRouteName;
922
    }
923
924
    public function getClass()
925
    {
926
        if ($this->hasActiveSubClass()) {
927
            if ($this->getParentFieldDescription()) {
928
                throw new \RuntimeException('Feature not implemented: an embedded admin cannot have subclass');
929
            }
930
931
            $subClass = $this->getRequest()->query->get('subclass');
932
933
            if (!$this->hasSubClass($subClass)) {
934
                throw new \RuntimeException(sprintf('Subclass "%s" is not defined.', $subClass));
935
            }
936
937
            return $this->getSubClass($subClass);
938
        }
939
940
        // see https://github.com/sonata-project/SonataCoreBundle/commit/247eeb0a7ca7211142e101754769d70bc402a5b4
941
        if ($this->subject && \is_object($this->subject)) {
942
            return ClassUtils::getClass($this->subject);
943
        }
944
945
        return $this->class;
946
    }
947
948
    public function getSubClasses(): array
949
    {
950
        return $this->subClasses;
951
    }
952
953
    /**
954
     * NEXT_MAJOR: remove this method.
955
     */
956
    public function addSubClass($subClass): void
957
    {
958
        @trigger_error(sprintf(
959
            'Method "%s" is deprecated since 3.30 and will be removed in 4.0.',
960
            __METHOD__
961
        ), E_USER_DEPRECATED);
962
963
        if (!\in_array($subClass, $this->subClasses, true)) {
964
            $this->subClasses[] = $subClass;
965
        }
966
    }
967
968
    public function setSubClasses(array $subClasses): void
969
    {
970
        $this->subClasses = $subClasses;
971
    }
972
973
    public function hasSubClass($name)
974
    {
975
        return isset($this->subClasses[$name]);
976
    }
977
978
    public function hasActiveSubClass()
979
    {
980
        if (\count($this->subClasses) > 0 && $this->request) {
981
            return null !== $this->getRequest()->query->get('subclass');
982
        }
983
984
        return false;
985
    }
986
987
    public function getActiveSubClass()
988
    {
989
        if (!$this->hasActiveSubClass()) {
990
            @trigger_error(sprintf(
991
                'Calling %s() when there is no active subclass is deprecated since sonata-project/admin-bundle 3.52 and will throw an exception in 4.0. '.
992
                'Use %s::hasActiveSubClass() to know if there is an active subclass.',
993
                __METHOD__,
994
                __CLASS__
995
            ), E_USER_DEPRECATED);
996
            // NEXT_MAJOR : remove the previous `trigger_error()` call, the `return null` statement, uncomment the following exception and declare string as return type
997
            // throw new \LogicException(sprintf(
998
            //    'Admin "%s" has no active subclass.',
999
            //    static::class
1000
            // ));
1001
1002
            return null;
1003
        }
1004
1005
        return $this->getSubClass($this->getActiveSubclassCode());
1006
    }
1007
1008
    public function getActiveSubclassCode()
1009
    {
1010
        if (!$this->hasActiveSubClass()) {
1011
            @trigger_error(sprintf(
1012
                'Calling %s() when there is no active subclass is deprecated since sonata-project/admin-bundle 3.52 and will throw an exception in 4.0. '.
1013
                'Use %s::hasActiveSubClass() to know if there is an active subclass.',
1014
                __METHOD__,
1015
                __CLASS__
1016
            ), E_USER_DEPRECATED);
1017
            // NEXT_MAJOR : remove the previous `trigger_error()` call, the `return null` statement, uncomment the following exception and declare string as return type
1018
            // throw new \LogicException(sprintf(
1019
            //    'Admin "%s" has no active subclass.',
1020
            //    static::class
1021
            // ));
1022
1023
            return null;
1024
        }
1025
1026
        $subClass = $this->getRequest()->query->get('subclass');
1027
1028
        if (!$this->hasSubClass($subClass)) {
1029
            @trigger_error(sprintf(
1030
                'Calling %s() when there is no active subclass is deprecated since sonata-project/admin-bundle 3.52 and will throw an exception in 4.0. '.
1031
                'Use %s::hasActiveSubClass() to know if there is an active subclass.',
1032
                __METHOD__,
1033
                __CLASS__
1034
            ), E_USER_DEPRECATED);
1035
            // NEXT_MAJOR : remove the previous `trigger_error()` call, the `return null` statement, uncomment the following exception and declare string as return type
1036
            // throw new \LogicException(sprintf(
1037
            //    'Admin "%s" has no active subclass.',
1038
            //    static::class
1039
            // ));
1040
1041
            return null;
1042
        }
1043
1044
        return $subClass;
1045
    }
1046
1047
    public function getBatchActions()
1048
    {
1049
        $actions = [];
1050
1051
        if ($this->hasRoute('delete') && $this->hasAccess('delete')) {
1052
            $actions['delete'] = [
1053
                'label' => 'action_delete',
1054
                'translation_domain' => 'SonataAdminBundle',
1055
                'ask_confirmation' => true, // by default always true
1056
            ];
1057
        }
1058
1059
        $actions = $this->configureBatchActions($actions);
1060
1061
        foreach ($this->getExtensions() as $extension) {
1062
            $actions = $extension->configureBatchActions($this, $actions);
1063
        }
1064
1065
        foreach ($actions  as $name => &$action) {
1066
            if (!\array_key_exists('label', $action)) {
1067
                $action['label'] = $this->getTranslationLabel($name, 'batch', 'label');
1068
            }
1069
1070
            if (!\array_key_exists('translation_domain', $action)) {
1071
                $action['translation_domain'] = $this->getTranslationDomain();
1072
            }
1073
        }
1074
1075
        return $actions;
1076
    }
1077
1078
    public function getRoutes()
1079
    {
1080
        $this->buildRoutes();
1081
1082
        return $this->routes;
1083
    }
1084
1085
    public function getRouterIdParameter()
1086
    {
1087
        return '{'.$this->getIdParameter().'}';
1088
    }
1089
1090
    public function getIdParameter()
1091
    {
1092
        $parameter = 'id';
1093
1094
        for ($i = 0; $i < $this->getChildDepth(); ++$i) {
1095
            $parameter = 'child'.ucfirst($parameter);
1096
        }
1097
1098
        return $parameter;
1099
    }
1100
1101
    public function hasRoute($name)
1102
    {
1103
        if (!$this->routeGenerator) {
1104
            throw new \RuntimeException('RouteGenerator cannot be null');
1105
        }
1106
1107
        return $this->routeGenerator->hasAdminRoute($this, $name);
1108
    }
1109
1110
    public function isCurrentRoute(string $name, ?string $adminCode = null): bool
1111
    {
1112
        if (!$this->hasRequest()) {
1113
            return false;
1114
        }
1115
1116
        $request = $this->getRequest();
1117
        $route = $request->get('_route');
1118
1119
        if ($adminCode) {
1120
            $admin = $this->getConfigurationPool()->getAdminByAdminCode($adminCode);
1121
        } else {
1122
            $admin = $this;
1123
        }
1124
1125
        if (!$admin) {
1126
            return false;
1127
        }
1128
1129
        return ($admin->getBaseRouteName().'_'.$name) === $route;
1130
    }
1131
1132
    public function generateObjectUrl($name, $object, array $parameters = [], $absolute = RoutingUrlGeneratorInterface::ABSOLUTE_PATH)
1133
    {
1134
        $parameters['id'] = $this->getUrlsafeIdentifier($object);
1135
1136
        return $this->generateUrl($name, $parameters, $absolute);
1137
    }
1138
1139
    public function generateUrl($name, array $parameters = [], $absolute = RoutingUrlGeneratorInterface::ABSOLUTE_PATH)
1140
    {
1141
        return $this->routeGenerator->generateUrl($this, $name, $parameters, $absolute);
1142
    }
1143
1144
    public function generateMenuUrl($name, array $parameters = [], $absolute = RoutingUrlGeneratorInterface::ABSOLUTE_PATH)
1145
    {
1146
        return $this->routeGenerator->generateMenuUrl($this, $name, $parameters, $absolute);
1147
    }
1148
1149
    final public function setTemplateRegistry(MutableTemplateRegistryInterface $templateRegistry): void
1150
    {
1151
        $this->templateRegistry = $templateRegistry;
1152
    }
1153
1154
    /**
1155
     * @param array<string, string> $templates
1156
     */
1157
    public function setTemplates(array $templates): void
1158
    {
1159
        // NEXT_MAJOR: Remove this line
1160
        $this->templates = $templates;
1161
1162
        $this->getTemplateRegistry()->setTemplates($templates);
1163
    }
1164
1165
    /**
1166
     * {@inheritdoc}
1167
     */
1168
    public function setTemplate($name, $template): void
1169
    {
1170
        // NEXT_MAJOR: Remove this line
1171
        $this->templates[$name] = $template;
1172
1173
        $this->getTemplateRegistry()->setTemplate($name, $template);
1174
    }
1175
1176
    /**
1177
     * @deprecated since 3.34, will be dropped in 4.0. Use TemplateRegistry services instead
1178
     *
1179
     * @return array<string, string>
1180
     */
1181
    public function getTemplates(): array
1182
    {
1183
        return $this->getTemplateRegistry()->getTemplates();
1184
    }
1185
1186
    /**
1187
     * @deprecated since 3.34, will be dropped in 4.0. Use TemplateRegistry services instead
1188
     *
1189
     * @param string $name
1190
     *
1191
     * @return string|null
1192
     */
1193
    public function getTemplate($name)
1194
    {
1195
        return $this->getTemplateRegistry()->getTemplate($name);
1196
    }
1197
1198
    public function getNewInstance()
1199
    {
1200
        $object = $this->getModelManager()->getModelInstance($this->getClass());
1201
        foreach ($this->getExtensions() as $extension) {
1202
            $extension->alterNewInstance($this, $object);
1203
        }
1204
1205
        return $object;
1206
    }
1207
1208
    public function getFormBuilder()
1209
    {
1210
        $this->formOptions['data_class'] = $this->getClass();
1211
1212
        $formBuilder = $this->getFormContractor()->getFormBuilder(
1213
            $this->getUniqid(),
1214
            $this->formOptions
1215
        );
1216
1217
        $this->defineFormBuilder($formBuilder);
1218
1219
        return $formBuilder;
1220
    }
1221
1222
    /**
1223
     * This method is being called by the main admin class and the child class,
1224
     * the getFormBuilder is only call by the main admin class.
1225
     */
1226
    public function defineFormBuilder(FormBuilderInterface $formBuilder): void
1227
    {
1228
        $mapper = new FormMapper($this->getFormContractor(), $formBuilder, $this);
1229
1230
        $this->configureFormFields($mapper);
1231
1232
        foreach ($this->getExtensions() as $extension) {
1233
            $extension->configureFormFields($mapper);
1234
        }
1235
1236
        $this->attachInlineValidator();
1237
    }
1238
1239
    public function attachAdminClass(FieldDescriptionInterface $fieldDescription): void
1240
    {
1241
        $pool = $this->getConfigurationPool();
1242
1243
        $adminCode = $fieldDescription->getOption('admin_code');
1244
1245
        if (null !== $adminCode) {
1246
            $admin = $pool->getAdminByAdminCode($adminCode);
1247
        } else {
1248
            $admin = $pool->getAdminByClass($fieldDescription->getTargetEntity());
1249
        }
1250
1251
        if (!$admin) {
1252
            return;
1253
        }
1254
1255
        if ($this->hasRequest()) {
1256
            $admin->setRequest($this->getRequest());
1257
        }
1258
1259
        $fieldDescription->setAssociationAdmin($admin);
1260
    }
1261
1262
    public function getObject($id)
1263
    {
1264
        $object = $this->getModelManager()->find($this->getClass(), $id);
1265
        foreach ($this->getExtensions() as $extension) {
1266
            $extension->alterObject($this, $object);
0 ignored issues
show
It seems like $object defined by $this->getModelManager()...$this->getClass(), $id) on line 1264 can also be of type null; however, Sonata\AdminBundle\Admin...nterface::alterObject() does only seem to accept object, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
1267
        }
1268
1269
        return $object;
1270
    }
1271
1272
    public function getForm()
1273
    {
1274
        $this->buildForm();
1275
1276
        return $this->form;
1277
    }
1278
1279
    public function getList()
1280
    {
1281
        $this->buildList();
1282
1283
        return $this->list;
1284
    }
1285
1286
    public function createQuery($context = 'list')
1287
    {
1288
        if (\func_num_args() > 0) {
1289
            @trigger_error(
1290
                'The $context argument of '.__METHOD__.' is deprecated since 3.3, to be removed in 4.0.',
1291
                E_USER_DEPRECATED
1292
            );
1293
        }
1294
        $query = $this->getModelManager()->createQuery($this->getClass());
1295
1296
        foreach ($this->extensions as $extension) {
1297
            $extension->configureQuery($this, $query, $context);
1298
        }
1299
1300
        return $query;
1301
    }
1302
1303
    public function getDatagrid()
1304
    {
1305
        $this->buildDatagrid();
1306
1307
        return $this->datagrid;
1308
    }
1309
1310
    public function buildTabMenu($action, AdminInterface $childAdmin = null): void
1311
    {
1312
        if ($this->loaded['tab_menu']) {
1313
            return;
1314
        }
1315
1316
        $this->loaded['tab_menu'] = true;
1317
1318
        $menu = $this->menuFactory->createItem('root');
1319
        $menu->setChildrenAttribute('class', 'nav navbar-nav');
1320
        $menu->setExtra('translation_domain', $this->translationDomain);
1321
1322
        // Prevents BC break with KnpMenuBundle v1.x
1323
        if (method_exists($menu, 'setCurrentUri')) {
1324
            $menu->setCurrentUri($this->getRequest()->getBaseUrl().$this->getRequest()->getPathInfo());
1325
        }
1326
1327
        $this->configureTabMenu($menu, $action, $childAdmin);
1328
1329
        foreach ($this->getExtensions() as $extension) {
1330
            $extension->configureTabMenu($this, $menu, $action, $childAdmin);
1331
        }
1332
1333
        $this->menu = $menu;
1334
    }
1335
1336
    /**
1337
     * @param string $action
1338
     *
1339
     * @return ItemInterface
1340
     */
1341
    public function getSideMenu($action, AdminInterface $childAdmin = null)
1342
    {
1343
        if ($this->isChild()) {
1344
            return $this->getParent()->getSideMenu($action, $this);
1345
        }
1346
1347
        $this->buildTabMenu($action, $childAdmin);
1348
1349
        return $this->menu;
1350
    }
1351
1352
    /**
1353
     * Returns the root code.
1354
     *
1355
     * @return string the root code
1356
     */
1357
    public function getRootCode()
1358
    {
1359
        return $this->getRoot()->getCode();
1360
    }
1361
1362
    /**
1363
     * Returns the master admin.
1364
     *
1365
     * @return AbstractAdmin the root admin class
1366
     */
1367
    public function getRoot()
1368
    {
1369
        $parentFieldDescription = $this->getParentFieldDescription();
1370
1371
        if (!$parentFieldDescription) {
1372
            return $this;
1373
        }
1374
1375
        return $parentFieldDescription->getAdmin()->getRoot();
1376
    }
1377
1378
    public function setBaseControllerName($baseControllerName): void
1379
    {
1380
        $this->baseControllerName = $baseControllerName;
1381
    }
1382
1383
    public function getBaseControllerName()
1384
    {
1385
        return $this->baseControllerName;
1386
    }
1387
1388
    /**
1389
     * @param string $label
1390
     */
1391
    public function setLabel($label): void
1392
    {
1393
        $this->label = $label;
1394
    }
1395
1396
    public function getLabel()
1397
    {
1398
        return $this->label;
1399
    }
1400
1401
    /**
1402
     * @param bool $persist
1403
     *
1404
     * NEXT_MAJOR: remove this method
1405
     *
1406
     * @deprecated since 3.34, to be removed in 4.0.
1407
     */
1408
    public function setPersistFilters($persist): void
1409
    {
1410
        @trigger_error(
1411
            'The '.__METHOD__.' method is deprecated since version 3.34 and will be removed in 4.0.',
1412
            E_USER_DEPRECATED
1413
        );
1414
1415
        $this->persistFilters = $persist;
1416
    }
1417
1418
    public function setFilterPersister(FilterPersisterInterface $filterPersister = null): void
1419
    {
1420
        $this->filterPersister = $filterPersister;
1421
        // NEXT_MAJOR remove the deprecated property will be removed. Needed for persisted filter condition.
1422
        $this->persistFilters = true;
1423
    }
1424
1425
    /**
1426
     * @param int $maxPerPage
1427
     */
1428
    public function setMaxPerPage($maxPerPage): void
1429
    {
1430
        $this->maxPerPage = $maxPerPage;
1431
    }
1432
1433
    /**
1434
     * @return int
1435
     */
1436
    public function getMaxPerPage()
1437
    {
1438
        return $this->maxPerPage;
1439
    }
1440
1441
    /**
1442
     * @param int $maxPageLinks
1443
     */
1444
    public function setMaxPageLinks($maxPageLinks): void
1445
    {
1446
        $this->maxPageLinks = $maxPageLinks;
1447
    }
1448
1449
    /**
1450
     * @return int
1451
     */
1452
    public function getMaxPageLinks()
1453
    {
1454
        return $this->maxPageLinks;
1455
    }
1456
1457
    public function getFormGroups()
1458
    {
1459
        return $this->formGroups;
1460
    }
1461
1462
    public function setFormGroups(array $formGroups): void
1463
    {
1464
        $this->formGroups = $formGroups;
1465
    }
1466
1467
    public function removeFieldFromFormGroup($key): void
1468
    {
1469
        foreach ($this->formGroups as $name => $formGroup) {
1470
            unset($this->formGroups[$name]['fields'][$key]);
1471
1472
            if (empty($this->formGroups[$name]['fields'])) {
1473
                unset($this->formGroups[$name]);
1474
            }
1475
        }
1476
    }
1477
1478
    /**
1479
     * @param array $group
1480
     */
1481
    public function reorderFormGroup($group, array $keys): void
1482
    {
1483
        $formGroups = $this->getFormGroups();
1484
        $formGroups[$group]['fields'] = array_merge(array_flip($keys), $formGroups[$group]['fields']);
1485
        $this->setFormGroups($formGroups);
1486
    }
1487
1488
    public function getFormTabs()
1489
    {
1490
        return $this->formTabs;
1491
    }
1492
1493
    public function setFormTabs(array $formTabs): void
1494
    {
1495
        $this->formTabs = $formTabs;
1496
    }
1497
1498
    public function getShowTabs()
1499
    {
1500
        return $this->showTabs;
1501
    }
1502
1503
    public function setShowTabs(array $showTabs): void
1504
    {
1505
        $this->showTabs = $showTabs;
1506
    }
1507
1508
    public function getShowGroups()
1509
    {
1510
        return $this->showGroups;
1511
    }
1512
1513
    public function setShowGroups(array $showGroups): void
1514
    {
1515
        $this->showGroups = $showGroups;
1516
    }
1517
1518
    public function reorderShowGroup($group, array $keys): void
1519
    {
1520
        $showGroups = $this->getShowGroups();
1521
        $showGroups[$group]['fields'] = array_merge(array_flip($keys), $showGroups[$group]['fields']);
1522
        $this->setShowGroups($showGroups);
1523
    }
1524
1525
    public function setParentFieldDescription(FieldDescriptionInterface $parentFieldDescription): void
1526
    {
1527
        $this->parentFieldDescription = $parentFieldDescription;
1528
    }
1529
1530
    public function getParentFieldDescription()
1531
    {
1532
        return $this->parentFieldDescription;
1533
    }
1534
1535
    public function hasParentFieldDescription()
1536
    {
1537
        return $this->parentFieldDescription instanceof FieldDescriptionInterface;
1538
    }
1539
1540
    public function setSubject($subject): void
1541
    {
1542
        if (\is_object($subject) && !is_a($subject, $this->getClass(), true)) {
1543
            $message = <<<'EOT'
1544
You are trying to set entity an instance of "%s",
1545
which is not the one registered with this admin class ("%s").
1546
This is deprecated since 3.5 and will no longer be supported in 4.0.
1547
EOT;
1548
1549
            @trigger_error(
1550
                sprintf($message, \get_class($subject), $this->getClass()),
1551
                E_USER_DEPRECATED
1552
            ); // NEXT_MAJOR : throw an exception instead
1553
        }
1554
1555
        $this->subject = $subject;
1556
    }
1557
1558
    public function getSubject()
1559
    {
1560
        if (null === $this->subject && $this->request && !$this->hasParentFieldDescription()) {
1561
            $id = $this->request->get($this->getIdParameter());
1562
1563
            if (null !== $id) {
1564
                $this->subject = $this->getObject($id);
1565
            }
1566
        }
1567
1568
        return $this->subject;
1569
    }
1570
1571
    public function hasSubject()
1572
    {
1573
        return (bool) $this->getSubject();
1574
    }
1575
1576
    public function getFormFieldDescriptions()
1577
    {
1578
        $this->buildForm();
1579
1580
        return $this->formFieldDescriptions;
1581
    }
1582
1583
    public function getFormFieldDescription($name)
1584
    {
1585
        return $this->hasFormFieldDescription($name) ? $this->formFieldDescriptions[$name] : null;
1586
    }
1587
1588
    /**
1589
     * Returns true if the admin has a FieldDescription with the given $name.
1590
     *
1591
     * @param string $name
1592
     *
1593
     * @return bool
1594
     */
1595
    public function hasFormFieldDescription($name)
1596
    {
1597
        return \array_key_exists($name, $this->formFieldDescriptions) ? true : false;
1598
    }
1599
1600
    public function addFormFieldDescription($name, FieldDescriptionInterface $fieldDescription): void
1601
    {
1602
        $this->formFieldDescriptions[$name] = $fieldDescription;
1603
    }
1604
1605
    /**
1606
     * remove a FieldDescription.
1607
     *
1608
     * @param string $name
1609
     */
1610
    public function removeFormFieldDescription($name): void
1611
    {
1612
        unset($this->formFieldDescriptions[$name]);
1613
    }
1614
1615
    /**
1616
     * build and return the collection of form FieldDescription.
1617
     *
1618
     * @return array collection of form FieldDescription
1619
     */
1620
    public function getShowFieldDescriptions()
1621
    {
1622
        $this->buildShow();
1623
1624
        return $this->showFieldDescriptions;
1625
    }
1626
1627
    /**
1628
     * Returns the form FieldDescription with the given $name.
1629
     *
1630
     * @param string $name
1631
     *
1632
     * @return FieldDescriptionInterface
1633
     */
1634
    public function getShowFieldDescription($name)
1635
    {
1636
        $this->buildShow();
1637
1638
        return $this->hasShowFieldDescription($name) ? $this->showFieldDescriptions[$name] : null;
1639
    }
1640
1641
    public function hasShowFieldDescription($name)
1642
    {
1643
        return \array_key_exists($name, $this->showFieldDescriptions);
1644
    }
1645
1646
    public function addShowFieldDescription($name, FieldDescriptionInterface $fieldDescription): void
1647
    {
1648
        $this->showFieldDescriptions[$name] = $fieldDescription;
1649
    }
1650
1651
    public function removeShowFieldDescription($name): void
1652
    {
1653
        unset($this->showFieldDescriptions[$name]);
1654
    }
1655
1656
    public function getListFieldDescriptions()
1657
    {
1658
        $this->buildList();
1659
1660
        return $this->listFieldDescriptions;
1661
    }
1662
1663
    public function getListFieldDescription($name)
1664
    {
1665
        return $this->hasListFieldDescription($name) ? $this->listFieldDescriptions[$name] : null;
1666
    }
1667
1668
    public function hasListFieldDescription($name)
1669
    {
1670
        $this->buildList();
1671
1672
        return \array_key_exists($name, $this->listFieldDescriptions) ? true : false;
1673
    }
1674
1675
    public function addListFieldDescription($name, FieldDescriptionInterface $fieldDescription): void
1676
    {
1677
        $this->listFieldDescriptions[$name] = $fieldDescription;
1678
    }
1679
1680
    public function removeListFieldDescription($name): void
1681
    {
1682
        unset($this->listFieldDescriptions[$name]);
1683
    }
1684
1685
    public function getFilterFieldDescription($name)
1686
    {
1687
        return $this->hasFilterFieldDescription($name) ? $this->filterFieldDescriptions[$name] : null;
1688
    }
1689
1690
    public function hasFilterFieldDescription($name)
1691
    {
1692
        return \array_key_exists($name, $this->filterFieldDescriptions) ? true : false;
1693
    }
1694
1695
    public function addFilterFieldDescription($name, FieldDescriptionInterface $fieldDescription): void
1696
    {
1697
        $this->filterFieldDescriptions[$name] = $fieldDescription;
1698
    }
1699
1700
    public function removeFilterFieldDescription($name): void
1701
    {
1702
        unset($this->filterFieldDescriptions[$name]);
1703
    }
1704
1705
    public function getFilterFieldDescriptions()
1706
    {
1707
        $this->buildDatagrid();
1708
1709
        return $this->filterFieldDescriptions;
1710
    }
1711
1712
    public function addChild(AdminInterface $child): void
1713
    {
1714
        for ($parentAdmin = $this; null !== $parentAdmin; $parentAdmin = $parentAdmin->getParent()) {
1715
            if ($parentAdmin->getCode() !== $child->getCode()) {
1716
                continue;
1717
            }
1718
1719
            throw new \RuntimeException(sprintf(
1720
                'Circular reference detected! The child admin `%s` is already in the parent tree of the `%s` admin.',
1721
                $child->getCode(), $this->getCode()
1722
            ));
1723
        }
1724
1725
        $this->children[$child->getCode()] = $child;
1726
1727
        $child->setParent($this);
1728
1729
        // NEXT_MAJOR: remove $args and add $field parameter to this function on next Major
1730
1731
        $args = \func_get_args();
1732
1733
        if (isset($args[1])) {
1734
            $child->addParentAssociationMapping($this->getCode(), $args[1]);
1735
        } else {
1736
            @trigger_error(
1737
                'Calling "addChild" without second argument is deprecated since 3.35'
1738
                .' and will not be allowed in 4.0.',
1739
                E_USER_DEPRECATED
1740
            );
1741
        }
1742
    }
1743
1744
    public function hasChild($code)
1745
    {
1746
        return isset($this->children[$code]);
1747
    }
1748
1749
    public function getChildren()
1750
    {
1751
        return $this->children;
1752
    }
1753
1754
    public function getChild($code)
1755
    {
1756
        return $this->hasChild($code) ? $this->children[$code] : null;
1757
    }
1758
1759
    public function setParent(AdminInterface $parent): void
1760
    {
1761
        $this->parent = $parent;
1762
    }
1763
1764
    public function getParent()
1765
    {
1766
        return $this->parent;
1767
    }
1768
1769
    final public function getRootAncestor()
1770
    {
1771
        $parent = $this;
1772
1773
        while ($parent->isChild()) {
1774
            $parent = $parent->getParent();
1775
        }
1776
1777
        return $parent;
1778
    }
1779
1780
    final public function getChildDepth()
1781
    {
1782
        $parent = $this;
1783
        $depth = 0;
1784
1785
        while ($parent->isChild()) {
1786
            $parent = $parent->getParent();
1787
            ++$depth;
1788
        }
1789
1790
        return $depth;
1791
    }
1792
1793
    final public function getCurrentLeafChildAdmin()
1794
    {
1795
        $child = $this->getCurrentChildAdmin();
1796
1797
        if (null === $child) {
1798
            return null;
1799
        }
1800
1801
        for ($c = $child; null !== $c; $c = $child->getCurrentChildAdmin()) {
1802
            $child = $c;
1803
        }
1804
1805
        return $child;
1806
    }
1807
1808
    public function isChild()
1809
    {
1810
        return $this->parent instanceof AdminInterface;
1811
    }
1812
1813
    /**
1814
     * Returns true if the admin has children, false otherwise.
1815
     *
1816
     * @return bool if the admin has children
1817
     */
1818
    public function hasChildren()
1819
    {
1820
        return \count($this->children) > 0;
1821
    }
1822
1823
    public function setUniqid($uniqid): void
1824
    {
1825
        $this->uniqid = $uniqid;
1826
    }
1827
1828
    public function getUniqid()
1829
    {
1830
        if (!$this->uniqid) {
1831
            $this->uniqid = 's'.substr(md5($this->getBaseCodeRoute()), 0, 10);
1832
        }
1833
1834
        return $this->uniqid;
1835
    }
1836
1837
    /**
1838
     * {@inheritdoc}
1839
     */
1840
    public function getClassnameLabel()
1841
    {
1842
        return $this->classnameLabel;
1843
    }
1844
1845
    public function getPersistentParameters()
1846
    {
1847
        $parameters = [];
1848
1849
        foreach ($this->getExtensions() as $extension) {
1850
            $params = $extension->getPersistentParameters($this);
1851
1852
            if (!\is_array($params)) {
1853
                throw new \RuntimeException(sprintf('The %s::getPersistentParameters must return an array', \get_class($extension)));
1854
            }
1855
1856
            $parameters = array_merge($parameters, $params);
1857
        }
1858
1859
        return $parameters;
1860
    }
1861
1862
    /**
1863
     * {@inheritdoc}
1864
     */
1865
    public function getPersistentParameter(string $name)
1866
    {
1867
        $parameters = $this->getPersistentParameters();
1868
1869
        return $parameters[$name] ?? null;
1870
    }
1871
1872
    public function setCurrentChild($currentChild): void
1873
    {
1874
        $this->currentChild = $currentChild;
1875
    }
1876
1877
    public function getCurrentChild()
1878
    {
1879
        return $this->currentChild;
1880
    }
1881
1882
    /**
1883
     * Returns the current child admin instance.
1884
     *
1885
     * @return AdminInterface|null the current child admin instance
1886
     */
1887
    public function getCurrentChildAdmin()
1888
    {
1889
        foreach ($this->children as $children) {
1890
            if ($children->getCurrentChild()) {
1891
                return $children;
1892
            }
1893
        }
1894
    }
1895
1896
    public function trans($id, array $parameters = [], $domain = null, $locale = null)
1897
    {
1898
        @trigger_error(
1899
            'The '.__METHOD__.' method is deprecated since version 3.9 and will be removed in 4.0.',
1900
            E_USER_DEPRECATED
1901
        );
1902
1903
        $domain = $domain ?: $this->getTranslationDomain();
1904
1905
        return $this->translator->trans($id, $parameters, $domain, $locale);
1906
    }
1907
1908
    /**
1909
     * Translate a message id.
1910
     *
1911
     * NEXT_MAJOR: remove this method
1912
     *
1913
     * @param string      $id
1914
     * @param int         $count
1915
     * @param string|null $domain
1916
     * @param string|null $locale
1917
     *
1918
     * @return string the translated string
1919
     *
1920
     * @deprecated since 3.9, to be removed with 4.0
1921
     */
1922
    public function transChoice($id, $count, array $parameters = [], $domain = null, $locale = null)
1923
    {
1924
        @trigger_error(
1925
            'The '.__METHOD__.' method is deprecated since version 3.9 and will be removed in 4.0.',
1926
            E_USER_DEPRECATED
1927
        );
1928
1929
        $domain = $domain ?: $this->getTranslationDomain();
1930
1931
        return $this->translator->transChoice($id, $count, $parameters, $domain, $locale);
1932
    }
1933
1934
    public function setTranslationDomain($translationDomain): void
1935
    {
1936
        $this->translationDomain = $translationDomain;
1937
    }
1938
1939
    public function getTranslationDomain()
1940
    {
1941
        return $this->translationDomain;
1942
    }
1943
1944
    /**
1945
     * {@inheritdoc}
1946
     *
1947
     * NEXT_MAJOR: remove this method
1948
     *
1949
     * @deprecated since 3.9, to be removed with 4.0
1950
     */
1951
    public function setTranslator(TranslatorInterface $translator): void
1952
    {
1953
        $args = \func_get_args();
1954
        if (isset($args[1]) && $args[1]) {
1955
            @trigger_error(
1956
                'The '.__METHOD__.' method is deprecated since version 3.9 and will be removed in 4.0.',
1957
                E_USER_DEPRECATED
1958
            );
1959
        }
1960
1961
        $this->translator = $translator;
1962
    }
1963
1964
    /**
1965
     * {@inheritdoc}
1966
     *
1967
     * NEXT_MAJOR: remove this method
1968
     *
1969
     * @deprecated since 3.9, to be removed with 4.0
1970
     */
1971
    public function getTranslator()
1972
    {
1973
        @trigger_error(
1974
            'The '.__METHOD__.' method is deprecated since version 3.9 and will be removed in 4.0.',
1975
            E_USER_DEPRECATED
1976
        );
1977
1978
        return $this->translator;
1979
    }
1980
1981
    public function getTranslationLabel($label, $context = '', $type = '')
1982
    {
1983
        return $this->getLabelTranslatorStrategy()->getLabel($label, $context, $type);
1984
    }
1985
1986
    public function setRequest(Request $request): void
1987
    {
1988
        $this->request = $request;
1989
1990
        foreach ($this->getChildren() as $children) {
1991
            $children->setRequest($request);
1992
        }
1993
    }
1994
1995
    public function getRequest()
1996
    {
1997
        if (!$this->request) {
1998
            throw new \RuntimeException('The Request object has not been set');
1999
        }
2000
2001
        return $this->request;
2002
    }
2003
2004
    public function hasRequest()
2005
    {
2006
        return null !== $this->request;
2007
    }
2008
2009
    public function setFormContractor(FormContractorInterface $formBuilder): void
2010
    {
2011
        $this->formContractor = $formBuilder;
2012
    }
2013
2014
    /**
2015
     * @return FormContractorInterface
2016
     */
2017
    public function getFormContractor()
2018
    {
2019
        return $this->formContractor;
2020
    }
2021
2022
    public function setDatagridBuilder(DatagridBuilderInterface $datagridBuilder): void
2023
    {
2024
        $this->datagridBuilder = $datagridBuilder;
2025
    }
2026
2027
    public function getDatagridBuilder()
2028
    {
2029
        return $this->datagridBuilder;
2030
    }
2031
2032
    public function setListBuilder(ListBuilderInterface $listBuilder): void
2033
    {
2034
        $this->listBuilder = $listBuilder;
2035
    }
2036
2037
    public function getListBuilder()
2038
    {
2039
        return $this->listBuilder;
2040
    }
2041
2042
    public function setShowBuilder(ShowBuilderInterface $showBuilder): void
2043
    {
2044
        $this->showBuilder = $showBuilder;
2045
    }
2046
2047
    /**
2048
     * @return ShowBuilderInterface
2049
     */
2050
    public function getShowBuilder()
2051
    {
2052
        return $this->showBuilder;
2053
    }
2054
2055
    public function setConfigurationPool(Pool $configurationPool): void
2056
    {
2057
        $this->configurationPool = $configurationPool;
2058
    }
2059
2060
    /**
2061
     * @return Pool
2062
     */
2063
    public function getConfigurationPool()
2064
    {
2065
        return $this->configurationPool;
2066
    }
2067
2068
    public function setRouteGenerator(RouteGeneratorInterface $routeGenerator): void
2069
    {
2070
        $this->routeGenerator = $routeGenerator;
2071
    }
2072
2073
    /**
2074
     * @return RouteGeneratorInterface
2075
     */
2076
    public function getRouteGenerator()
2077
    {
2078
        return $this->routeGenerator;
2079
    }
2080
2081
    public function getCode()
2082
    {
2083
        return $this->code;
2084
    }
2085
2086
    public function getBaseCodeRoute()
2087
    {
2088
        if ($this->isChild()) {
2089
            return $this->getParent()->getBaseCodeRoute().'|'.$this->getCode();
2090
        }
2091
2092
        return $this->getCode();
2093
    }
2094
2095
    public function getModelManager()
2096
    {
2097
        return $this->modelManager;
2098
    }
2099
2100
    public function setModelManager(ModelManagerInterface $modelManager): void
2101
    {
2102
        $this->modelManager = $modelManager;
2103
    }
2104
2105
    public function getManagerType()
2106
    {
2107
        return $this->managerType;
2108
    }
2109
2110
    /**
2111
     * @param string $type
2112
     */
2113
    public function setManagerType($type): void
2114
    {
2115
        $this->managerType = $type;
2116
    }
2117
2118
    public function getObjectIdentifier()
2119
    {
2120
        return $this->getCode();
2121
    }
2122
2123
    /**
2124
     * Set the roles and permissions per role.
2125
     */
2126
    public function setSecurityInformation(array $information): void
2127
    {
2128
        $this->securityInformation = $information;
2129
    }
2130
2131
    public function getSecurityInformation()
2132
    {
2133
        return $this->securityInformation;
2134
    }
2135
2136
    /**
2137
     * Return the list of permissions the user should have in order to display the admin.
2138
     *
2139
     * @param string $context
2140
     *
2141
     * @return array
2142
     */
2143
    public function getPermissionsShow($context)
2144
    {
2145
        switch ($context) {
2146
            case self::CONTEXT_DASHBOARD:
2147
            case self::CONTEXT_MENU:
2148
            default:
2149
                return ['LIST'];
2150
        }
2151
    }
2152
2153
    public function showIn($context)
2154
    {
2155
        switch ($context) {
2156
            case self::CONTEXT_DASHBOARD:
2157
            case self::CONTEXT_MENU:
2158
            default:
2159
                return $this->isGranted($this->getPermissionsShow($context));
2160
        }
2161
    }
2162
2163
    public function createObjectSecurity($object): void
2164
    {
2165
        $this->getSecurityHandler()->createObjectSecurity($this, $object);
2166
    }
2167
2168
    public function setSecurityHandler(SecurityHandlerInterface $securityHandler): void
2169
    {
2170
        $this->securityHandler = $securityHandler;
2171
    }
2172
2173
    public function getSecurityHandler()
2174
    {
2175
        return $this->securityHandler;
2176
    }
2177
2178
    public function isGranted($name, $object = null)
2179
    {
2180
        $key = md5(json_encode($name).($object ? '/'.spl_object_hash($object) : ''));
2181
2182
        if (!\array_key_exists($key, $this->cacheIsGranted)) {
2183
            $this->cacheIsGranted[$key] = $this->securityHandler->isGranted($this, $name, $object ?: $this);
2184
        }
2185
2186
        return $this->cacheIsGranted[$key];
2187
    }
2188
2189
    public function getUrlsafeIdentifier($entity)
2190
    {
2191
        return $this->getModelManager()->getUrlsafeIdentifier($entity);
2192
    }
2193
2194
    public function getNormalizedIdentifier($entity)
2195
    {
2196
        return $this->getModelManager()->getNormalizedIdentifier($entity);
2197
    }
2198
2199
    public function id($entity)
2200
    {
2201
        return $this->getNormalizedIdentifier($entity);
2202
    }
2203
2204
    public function setValidator($validator): void
2205
    {
2206
        // NEXT_MAJOR: Move ValidatorInterface check to method signature
2207
        if (!$validator instanceof ValidatorInterface) {
2208
            throw new \InvalidArgumentException(
2209
                'Argument 1 must be an instance of Symfony\Component\Validator\Validator\ValidatorInterface'
2210
            );
2211
        }
2212
2213
        $this->validator = $validator;
2214
    }
2215
2216
    public function getValidator()
2217
    {
2218
        return $this->validator;
2219
    }
2220
2221
    public function getShow()
2222
    {
2223
        $this->buildShow();
2224
2225
        return $this->show;
2226
    }
2227
2228
    public function setFormTheme(array $formTheme): void
2229
    {
2230
        $this->formTheme = $formTheme;
2231
    }
2232
2233
    public function getFormTheme()
2234
    {
2235
        return $this->formTheme;
2236
    }
2237
2238
    public function setFilterTheme(array $filterTheme): void
2239
    {
2240
        $this->filterTheme = $filterTheme;
2241
    }
2242
2243
    public function getFilterTheme()
2244
    {
2245
        return $this->filterTheme;
2246
    }
2247
2248
    public function addExtension(AdminExtensionInterface $extension): void
2249
    {
2250
        $this->extensions[] = $extension;
2251
    }
2252
2253
    public function getExtensions()
2254
    {
2255
        return $this->extensions;
2256
    }
2257
2258
    public function setMenuFactory(MenuFactoryInterface $menuFactory): void
2259
    {
2260
        $this->menuFactory = $menuFactory;
2261
    }
2262
2263
    public function getMenuFactory()
2264
    {
2265
        return $this->menuFactory;
2266
    }
2267
2268
    public function setRouteBuilder(RouteBuilderInterface $routeBuilder): void
2269
    {
2270
        $this->routeBuilder = $routeBuilder;
2271
    }
2272
2273
    public function getRouteBuilder()
2274
    {
2275
        return $this->routeBuilder;
2276
    }
2277
2278
    public function toString($object)
2279
    {
2280
        if (!\is_object($object)) {
2281
            return '';
2282
        }
2283
2284
        if (method_exists($object, '__toString') && null !== $object->__toString()) {
2285
            return (string) $object;
2286
        }
2287
2288
        return sprintf('%s:%s', ClassUtils::getClass($object), spl_object_hash($object));
2289
    }
2290
2291
    public function setLabelTranslatorStrategy(LabelTranslatorStrategyInterface $labelTranslatorStrategy): void
2292
    {
2293
        $this->labelTranslatorStrategy = $labelTranslatorStrategy;
2294
    }
2295
2296
    public function getLabelTranslatorStrategy()
2297
    {
2298
        return $this->labelTranslatorStrategy;
2299
    }
2300
2301
    public function supportsPreviewMode()
2302
    {
2303
        return $this->supportsPreviewMode;
2304
    }
2305
2306
    /**
2307
     * Set custom per page options.
2308
     */
2309
    public function setPerPageOptions(array $options): void
2310
    {
2311
        $this->perPageOptions = $options;
2312
    }
2313
2314
    /**
2315
     * Returns predefined per page options.
2316
     *
2317
     * @return array
2318
     */
2319
    public function getPerPageOptions()
2320
    {
2321
        return $this->perPageOptions;
2322
    }
2323
2324
    /**
2325
     * Set pager type.
2326
     *
2327
     * @param string $pagerType
2328
     */
2329
    public function setPagerType($pagerType): void
2330
    {
2331
        $this->pagerType = $pagerType;
2332
    }
2333
2334
    /**
2335
     * Get pager type.
2336
     *
2337
     * @return string
2338
     */
2339
    public function getPagerType()
2340
    {
2341
        return $this->pagerType;
2342
    }
2343
2344
    /**
2345
     * Returns true if the per page value is allowed, false otherwise.
2346
     *
2347
     * @param int $perPage
2348
     *
2349
     * @return bool
2350
     */
2351
    public function determinedPerPageValue($perPage)
2352
    {
2353
        return \in_array($perPage, $this->perPageOptions, true);
2354
    }
2355
2356
    public function isAclEnabled()
2357
    {
2358
        return $this->getSecurityHandler() instanceof AclSecurityHandlerInterface;
2359
    }
2360
2361
    public function getObjectMetadata($object)
2362
    {
2363
        return new Metadata($this->toString($object));
2364
    }
2365
2366
    public function getListModes()
2367
    {
2368
        return $this->listModes;
2369
    }
2370
2371
    public function setListMode($mode): void
2372
    {
2373
        if (!$this->hasRequest()) {
2374
            throw new \RuntimeException(sprintf('No request attached to the current admin: %s', $this->getCode()));
2375
        }
2376
2377
        $this->getRequest()->getSession()->set(sprintf('%s.list_mode', $this->getCode()), $mode);
2378
    }
2379
2380
    public function getListMode()
2381
    {
2382
        if (!$this->hasRequest()) {
2383
            return 'list';
2384
        }
2385
2386
        return $this->getRequest()->getSession()->get(sprintf('%s.list_mode', $this->getCode()), 'list');
2387
    }
2388
2389
    public function getAccessMapping()
2390
    {
2391
        return $this->accessMapping;
2392
    }
2393
2394
    public function checkAccess($action, $object = null): void
2395
    {
2396
        $access = $this->getAccess();
2397
2398
        if (!\array_key_exists($action, $access)) {
2399
            throw new \InvalidArgumentException(sprintf(
2400
                'Action "%s" could not be found in access mapping.'
2401
                .' Please make sure your action is defined into your admin class accessMapping property.',
2402
                $action
2403
            ));
2404
        }
2405
2406
        if (!\is_array($access[$action])) {
2407
            $access[$action] = [$access[$action]];
2408
        }
2409
2410
        foreach ($access[$action] as $role) {
2411
            if (false === $this->isGranted($role, $object)) {
2412
                throw new AccessDeniedException(sprintf('Access Denied to the action %s and role %s', $action, $role));
2413
            }
2414
        }
2415
    }
2416
2417
    /**
2418
     * {@inheritdoc}
2419
     */
2420
    public function hasAccess(string $action, $object = null): bool
2421
    {
2422
        $access = $this->getAccess();
2423
2424
        if (!\array_key_exists($action, $access)) {
2425
            return false;
2426
        }
2427
2428
        if (!\is_array($access[$action])) {
2429
            $access[$action] = [$access[$action]];
2430
        }
2431
2432
        foreach ($access[$action] as $role) {
2433
            if (false === $this->isGranted($role, $object)) {
2434
                return false;
2435
            }
2436
        }
2437
2438
        return true;
2439
    }
2440
2441
    /**
2442
     * @param object|null $object
2443
     */
2444
    final public function getActionButtons(string $action, $object = null): array
2445
    {
2446
        $buttonList = [];
2447
2448
        if (\in_array($action, ['tree', 'show', 'edit', 'delete', 'list', 'batch'], true)
2449
            && $this->hasAccess('create')
2450
            && $this->hasRoute('create')
2451
        ) {
2452
            $buttonList['create'] = [
2453
                'template' => $this->getTemplateRegistry()->getTemplate('button_create'),
2454
            ];
2455
        }
2456
2457
        if (\in_array($action, ['show', 'delete', 'acl', 'history'], true)
2458
            && $this->canAccessObject('edit', $object)
0 ignored issues
show
It seems like $object defined by parameter $object on line 2444 can also be of type null; however, Sonata\AdminBundle\Admin...dmin::canAccessObject() does only seem to accept object, maybe add an additional type check?

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

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

An additional type check may prevent trouble.

Loading history...
2459
            && $this->hasRoute('edit')
2460
        ) {
2461
            $buttonList['edit'] = [
2462
                'template' => $this->getTemplateRegistry()->getTemplate('button_edit'),
2463
            ];
2464
        }
2465
2466
        if (\in_array($action, ['show', 'edit', 'acl'], true)
2467
            && $this->canAccessObject('history', $object)
0 ignored issues
show
It seems like $object defined by parameter $object on line 2444 can also be of type null; however, Sonata\AdminBundle\Admin...dmin::canAccessObject() does only seem to accept object, maybe add an additional type check?

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

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

An additional type check may prevent trouble.

Loading history...
2468
            && $this->hasRoute('history')
2469
        ) {
2470
            $buttonList['history'] = [
2471
                'template' => $this->getTemplateRegistry()->getTemplate('button_history'),
2472
            ];
2473
        }
2474
2475
        if (\in_array($action, ['edit', 'history'], true)
2476
            && $this->isAclEnabled()
2477
            && $this->canAccessObject('acl', $object)
0 ignored issues
show
It seems like $object defined by parameter $object on line 2444 can also be of type null; however, Sonata\AdminBundle\Admin...dmin::canAccessObject() does only seem to accept object, maybe add an additional type check?

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

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

An additional type check may prevent trouble.

Loading history...
2478
            && $this->hasRoute('acl')
2479
        ) {
2480
            $buttonList['acl'] = [
2481
                'template' => $this->getTemplateRegistry()->getTemplate('button_acl'),
2482
            ];
2483
        }
2484
2485
        if (\in_array($action, ['edit', 'history', 'acl'], true)
2486
            && $this->canAccessObject('show', $object)
0 ignored issues
show
It seems like $object defined by parameter $object on line 2444 can also be of type null; however, Sonata\AdminBundle\Admin...dmin::canAccessObject() does only seem to accept object, maybe add an additional type check?

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

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

An additional type check may prevent trouble.

Loading history...
2487
            && \count($this->getShow()) > 0
2488
            && $this->hasRoute('show')
2489
        ) {
2490
            $buttonList['show'] = [
2491
                'template' => $this->getTemplateRegistry()->getTemplate('button_show'),
2492
            ];
2493
        }
2494
2495
        if (\in_array($action, ['show', 'edit', 'delete', 'acl', 'batch'], true)
2496
            && $this->hasAccess('list')
2497
            && $this->hasRoute('list')
2498
        ) {
2499
            $buttonList['list'] = [
2500
                'template' => $this->getTemplateRegistry()->getTemplate('button_list'),
2501
            ];
2502
        }
2503
2504
        $buttonList = $this->configureActionButtons($buttonList, $action, $object);
2505
2506
        foreach ($this->getExtensions() as $extension) {
2507
            $buttonList = $extension->configureActionButtons($this, $buttonList, $action, $object);
0 ignored issues
show
It seems like $object defined by parameter $object on line 2444 can also be of type null; however, Sonata\AdminBundle\Admin...onfigureActionButtons() does only seem to accept object, maybe add an additional type check?

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

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

An additional type check may prevent trouble.

Loading history...
2508
        }
2509
2510
        return $buttonList;
2511
    }
2512
2513
    /**
2514
     * {@inheritdoc}
2515
     */
2516
    public function getDashboardActions()
2517
    {
2518
        $actions = [];
2519
2520
        if ($this->hasRoute('create') && $this->hasAccess('create')) {
2521
            $actions['create'] = [
2522
                'label' => 'link_add',
2523
                'translation_domain' => 'SonataAdminBundle',
2524
                // NEXT_MAJOR: Remove this line and use commented line below it instead
2525
                'template' => $this->getTemplate('action_create'),
2526
                // 'template' => $this->getTemplateRegistry()->getTemplate('action_create'),
2527
                'url' => $this->generateUrl('create'),
2528
                'icon' => 'plus-circle',
2529
            ];
2530
        }
2531
2532
        if ($this->hasRoute('list') && $this->hasAccess('list')) {
2533
            $actions['list'] = [
2534
                'label' => 'link_list',
2535
                'translation_domain' => 'SonataAdminBundle',
2536
                'url' => $this->generateUrl('list'),
2537
                'icon' => 'list',
2538
            ];
2539
        }
2540
2541
        return $actions;
2542
    }
2543
2544
    /**
2545
     * {@inheritdoc}
2546
     */
2547
    final public function showMosaicButton($isShown): void
2548
    {
2549
        if ($isShown) {
2550
            $this->listModes['mosaic'] = ['class' => static::MOSAIC_ICON_CLASS];
2551
        } else {
2552
            unset($this->listModes['mosaic']);
2553
        }
2554
    }
2555
2556
    /**
2557
     * @param object $object
2558
     */
2559
    final public function getSearchResultLink($object): string
2560
    {
2561
        foreach ($this->searchResultActions as $action) {
2562
            if ($this->hasRoute($action) && $this->hasAccess($action, $object)) {
2563
                return $this->generateObjectUrl($action, $object);
2564
            }
2565
        }
2566
    }
2567
2568
    /**
2569
     * Checks if a filter type is set to a default value.
2570
     */
2571
    final public function isDefaultFilter(string $name): bool
2572
    {
2573
        $filter = $this->getFilterParameters();
2574
        $default = $this->getDefaultFilterValues();
2575
2576
        if (!\array_key_exists($name, $filter) || !\array_key_exists($name, $default)) {
2577
            return false;
2578
        }
2579
2580
        return $filter[$name] === $default[$name];
2581
    }
2582
2583
    /**
2584
     * Check object existence and access, without throw Exception.
2585
     *
2586
     * @param string $action
2587
     * @param object $object
2588
     *
2589
     * @return bool
2590
     */
2591
    public function canAccessObject($action, $object)
2592
    {
2593
        return $object && $this->id($object) && $this->hasAccess($action, $object);
2594
    }
2595
2596
    /**
2597
     * Hook to run after initilization.
2598
     */
2599
    protected function configure(): void
2600
    {
2601
    }
2602
2603
    /**
2604
     * urlize the given word.
2605
     *
2606
     * @param string $word
2607
     * @param string $sep  the separator
2608
     *
2609
     * @return string
2610
     */
2611
    final protected function urlize($word, $sep = '_')
2612
    {
2613
        return strtolower(preg_replace('/[^a-z0-9_]/i', $sep.'$1', $word));
2614
    }
2615
2616
    final protected function getTemplateRegistry(): MutableTemplateRegistryInterface
2617
    {
2618
        return $this->templateRegistry;
2619
    }
2620
2621
    /**
2622
     * Returns a list of default filters.
2623
     *
2624
     * @return array
2625
     */
2626
    final protected function getDefaultFilterValues()
2627
    {
2628
        $defaultFilterValues = [];
2629
2630
        $this->configureDefaultFilterValues($defaultFilterValues);
2631
2632
        foreach ($this->getExtensions() as $extension) {
2633
            // NEXT_MAJOR: remove method check in next major release
2634
            if (method_exists($extension, 'configureDefaultFilterValues')) {
2635
                $extension->configureDefaultFilterValues($this, $defaultFilterValues);
2636
            }
2637
        }
2638
2639
        return $defaultFilterValues;
2640
    }
2641
2642
    protected function configureFormFields(FormMapper $form): void
2643
    {
2644
    }
2645
2646
    protected function configureListFields(ListMapper $list): void
2647
    {
2648
    }
2649
2650
    protected function configureDatagridFilters(DatagridMapper $filter): void
2651
    {
2652
    }
2653
2654
    protected function configureShowFields(ShowMapper $show): void
2655
    {
2656
    }
2657
2658
    protected function configureRoutes(RouteCollection $collection): void
2659
    {
2660
    }
2661
2662
    protected function configureActionButtons($buttonList, $action, $object = null)
2663
    {
2664
        return $buttonList;
2665
    }
2666
2667
    /**
2668
     * Allows you to customize batch actions.
2669
     *
2670
     * @param array $actions List of actions
2671
     *
2672
     * @return array
2673
     */
2674
    protected function configureBatchActions($actions)
2675
    {
2676
        return $actions;
2677
    }
2678
2679
    /**
2680
     * NEXT_MAJOR: remove this method.
2681
     *
2682
     * @deprecated Use configureTabMenu instead
2683
     */
2684
    protected function configureSideMenu(MenuItemInterface $menu, $action, AdminInterface $childAdmin = null)
2685
    {
2686
    }
2687
2688
    /**
2689
     * Configures the tab menu in your admin.
2690
     *
2691
     * @param string $action
2692
     */
2693
    protected function configureTabMenu(MenuItemInterface $menu, $action, AdminInterface $childAdmin = null)
2694
    {
2695
        // Use configureSideMenu not to mess with previous overrides
2696
        // TODO remove once deprecation period is over
2697
        $this->configureSideMenu($menu, $action, $childAdmin);
2698
    }
2699
2700
    /**
2701
     * build the view FieldDescription array.
2702
     */
2703
    protected function buildShow(): void
2704
    {
2705
        if ($this->show) {
2706
            return;
2707
        }
2708
2709
        $this->show = new FieldDescriptionCollection();
2710
        $mapper = new ShowMapper($this->showBuilder, $this->show, $this);
2711
2712
        $this->configureShowFields($mapper);
2713
2714
        foreach ($this->getExtensions() as $extension) {
2715
            $extension->configureShowFields($mapper);
2716
        }
2717
    }
2718
2719
    /**
2720
     * build the list FieldDescription array.
2721
     */
2722
    protected function buildList(): void
2723
    {
2724
        if ($this->list) {
2725
            return;
2726
        }
2727
2728
        $this->list = $this->getListBuilder()->getBaseList();
2729
2730
        $mapper = new ListMapper($this->getListBuilder(), $this->list, $this);
2731
2732
        if (\count($this->getBatchActions()) > 0 && $this->hasRequest() && !$this->getRequest()->isXmlHttpRequest()) {
2733
            $fieldDescription = $this->getModelManager()->getNewFieldDescriptionInstance(
2734
                $this->getClass(),
2735
                'batch',
2736
                [
2737
                    'label' => 'batch',
2738
                    'code' => '_batch',
2739
                    'sortable' => false,
2740
                    'virtual_field' => true,
2741
                ]
2742
            );
2743
2744
            $fieldDescription->setAdmin($this);
2745
            // NEXT_MAJOR: Remove this line and use commented line below it instead
2746
            $fieldDescription->setTemplate($this->getTemplate('batch'));
2747
            // $fieldDescription->setTemplate($this->getTemplateRegistry()->getTemplate('batch'));
2748
2749
            $mapper->add($fieldDescription, 'batch');
2750
        }
2751
2752
        $this->configureListFields($mapper);
2753
2754
        foreach ($this->getExtensions() as $extension) {
2755
            $extension->configureListFields($mapper);
2756
        }
2757
2758
        if ($this->hasRequest() && $this->getRequest()->isXmlHttpRequest()) {
2759
            $fieldDescription = $this->getModelManager()->getNewFieldDescriptionInstance(
2760
                $this->getClass(),
2761
                'select',
2762
                [
2763
                    'label' => false,
2764
                    'code' => '_select',
2765
                    'sortable' => false,
2766
                    'virtual_field' => false,
2767
                ]
2768
            );
2769
2770
            $fieldDescription->setAdmin($this);
2771
            // NEXT_MAJOR: Remove this line and use commented line below it instead
2772
            $fieldDescription->setTemplate($this->getTemplate('select'));
2773
            // $fieldDescription->setTemplate($this->getTemplateRegistry()->getTemplate('select'));
2774
2775
            $mapper->add($fieldDescription, 'select');
2776
        }
2777
    }
2778
2779
    /**
2780
     * Build the form FieldDescription collection.
2781
     */
2782
    protected function buildForm(): void
2783
    {
2784
        if ($this->form) {
2785
            return;
2786
        }
2787
2788
        // append parent object if any
2789
        // todo : clean the way the Admin class can retrieve set the object
2790
        if ($this->isChild() && $this->getParentAssociationMapping()) {
2791
            $parent = $this->getParent()->getObject($this->request->get($this->getParent()->getIdParameter()));
2792
2793
            $propertyAccessor = $this->getConfigurationPool()->getPropertyAccessor();
2794
            $propertyPath = new PropertyPath($this->getParentAssociationMapping());
2795
2796
            $object = $this->getSubject();
2797
2798
            $value = $propertyAccessor->getValue($object, $propertyPath);
2799
2800
            if (\is_array($value) || ($value instanceof \Traversable && $value instanceof \ArrayAccess)) {
2801
                $value[] = $parent;
2802
                $propertyAccessor->setValue($object, $propertyPath, $value);
2803
            } else {
2804
                $propertyAccessor->setValue($object, $propertyPath, $parent);
2805
            }
2806
        }
2807
2808
        $formBuilder = $this->getFormBuilder();
2809
        $formBuilder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event): void {
2810
            $this->preValidate($event->getData());
2811
        }, 100);
2812
2813
        $this->form = $formBuilder->getForm();
2814
    }
2815
2816
    /**
2817
     * Gets the subclass corresponding to the given name.
2818
     *
2819
     * @param string $name The name of the sub class
2820
     *
2821
     * @return string the subclass
2822
     */
2823
    protected function getSubClass($name)
2824
    {
2825
        if ($this->hasSubClass($name)) {
2826
            return $this->subClasses[$name];
2827
        }
2828
2829
        throw new \RuntimeException(sprintf(
2830
            'Unable to find the subclass `%s` for admin `%s`',
2831
            $name,
2832
            static::class
2833
        ));
2834
    }
2835
2836
    /**
2837
     * Attach the inline validator to the model metadata, this must be done once per admin.
2838
     */
2839
    protected function attachInlineValidator(): void
2840
    {
2841
        $admin = $this;
2842
2843
        // add the custom inline validation option
2844
        $metadata = $this->validator->getMetadataFor($this->getClass());
2845
2846
        $metadata->addConstraint(new InlineConstraint([
2847
            'service' => $this,
2848
            'method' => static function (ErrorElement $errorElement, $object) use ($admin): void {
2849
                /* @var \Sonata\AdminBundle\Admin\AdminInterface $admin */
2850
2851
                // This avoid the main validation to be cascaded to children
2852
                // The problem occurs when a model Page has a collection of Page as property
2853
                if ($admin->hasSubject() && spl_object_hash($object) !== spl_object_hash($admin->getSubject())) {
2854
                    return;
2855
                }
2856
2857
                $admin->validate($errorElement, $object);
2858
2859
                foreach ($admin->getExtensions() as $extension) {
2860
                    $extension->validate($admin, $errorElement, $object);
2861
                }
2862
            },
2863
            'serializingWarning' => true,
2864
        ]));
2865
    }
2866
2867
    /**
2868
     * Predefine per page options.
2869
     */
2870
    protected function predefinePerPageOptions(): void
2871
    {
2872
        array_unshift($this->perPageOptions, $this->maxPerPage);
2873
        $this->perPageOptions = array_unique($this->perPageOptions);
2874
        sort($this->perPageOptions);
2875
    }
2876
2877
    /**
2878
     * Return list routes with permissions name.
2879
     *
2880
     * @return array<string, string>
2881
     */
2882
    protected function getAccess()
2883
    {
2884
        $access = array_merge([
2885
            'acl' => 'MASTER',
2886
            'export' => 'EXPORT',
2887
            'historyCompareRevisions' => 'EDIT',
2888
            'historyViewRevision' => 'EDIT',
2889
            'history' => 'EDIT',
2890
            'edit' => 'EDIT',
2891
            'show' => 'VIEW',
2892
            'create' => 'CREATE',
2893
            'delete' => 'DELETE',
2894
            'batchDelete' => 'DELETE',
2895
            'list' => 'LIST',
2896
        ], $this->getAccessMapping());
2897
2898
        foreach ($this->extensions as $extension) {
2899
            $access = array_merge($access, $extension->getAccessMapping($this));
2900
        }
2901
2902
        return $access;
2903
    }
2904
2905
    /**
2906
     * Configures a list of default filters.
2907
     */
2908
    protected function configureDefaultFilterValues(array &$filterValues): void
2909
    {
2910
    }
2911
2912
    /**
2913
     * {@inheritdoc}
2914
     */
2915
    private function buildDatagrid(): void
2916
    {
2917
        if ($this->datagrid) {
2918
            return;
2919
        }
2920
2921
        $filterParameters = $this->getFilterParameters();
2922
2923
        // transform _sort_by from a string to a FieldDescriptionInterface for the datagrid.
2924
        if (isset($filterParameters['_sort_by']) && \is_string($filterParameters['_sort_by'])) {
2925
            if ($this->hasListFieldDescription($filterParameters['_sort_by'])) {
2926
                $filterParameters['_sort_by'] = $this->getListFieldDescription($filterParameters['_sort_by']);
2927
            } else {
2928
                $filterParameters['_sort_by'] = $this->getModelManager()->getNewFieldDescriptionInstance(
2929
                    $this->getClass(),
2930
                    $filterParameters['_sort_by'],
2931
                    []
2932
                );
2933
2934
                $this->getListBuilder()->buildField(null, $filterParameters['_sort_by'], $this);
2935
            }
2936
        }
2937
2938
        // initialize the datagrid
2939
        $this->datagrid = $this->getDatagridBuilder()->getBaseDatagrid($this, $filterParameters);
2940
2941
        $this->datagrid->getPager()->setMaxPageLinks($this->maxPageLinks);
2942
2943
        $mapper = new DatagridMapper($this->getDatagridBuilder(), $this->datagrid, $this);
2944
2945
        // build the datagrid filter
2946
        $this->configureDatagridFilters($mapper);
2947
2948
        // ok, try to limit to add parent filter
2949
        if ($this->isChild() && $this->getParentAssociationMapping() && !$mapper->has($this->getParentAssociationMapping())) {
2950
            $mapper->add($this->getParentAssociationMapping(), null, [
2951
                'show_filter' => false,
2952
                'label' => false,
2953
                'field_type' => ModelHiddenType::class,
2954
                'field_options' => [
2955
                    'model_manager' => $this->getModelManager(),
2956
                ],
2957
                'operator_type' => HiddenType::class,
2958
            ], null, null, [
2959
                'admin_code' => $this->getParent()->getCode(),
2960
            ]);
2961
        }
2962
2963
        foreach ($this->getExtensions() as $extension) {
2964
            $extension->configureDatagridFilters($mapper);
2965
        }
2966
    }
2967
2968
    /**
2969
     * Build all the related urls to the current admin.
2970
     */
2971
    private function buildRoutes(): void
2972
    {
2973
        if ($this->loaded['routes']) {
2974
            return;
2975
        }
2976
2977
        $this->loaded['routes'] = true;
2978
2979
        $this->routes = new RouteCollection(
2980
            $this->getBaseCodeRoute(),
2981
            $this->getBaseRouteName(),
2982
            $this->getBaseRoutePattern(),
2983
            $this->getBaseControllerName()
2984
        );
2985
2986
        $this->routeBuilder->build($this, $this->routes);
2987
2988
        $this->configureRoutes($this->routes);
2989
2990
        foreach ($this->getExtensions() as $extension) {
2991
            $extension->configureRoutes($this, $this->routes);
2992
        }
2993
    }
2994
}
2995
2996
class_exists(\Sonata\Form\Validator\ErrorElement::class);
2997