AbstractAdmin::configureActionButtons()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

Loading history...
545
546
            // NEXT_MAJOR: Remove the following code in favor of the commented one.
547
            // If a key is provided we use it otherwise we use the generated label.
548
            // $fieldKey = \is_string($key) ? $key : $transLabel;
549
            // $fields[$fieldKey] = $field;
550
            if ($transLabel === $label) {
551
                $fields[$key] = $field;
552
            } else {
553
                $fields[$transLabel] = $field;
554
            }
555
        }
556
557
        return $this->getModelManager()->getDataSourceIterator($datagrid, $fields);
558
    }
559
560
    public function validate(ErrorElement $errorElement, $object): void
561
    {
562
    }
563
564
    /**
565
     * define custom variable.
566
     */
567
    public function initialize(): void
568
    {
569
        if (!$this->classnameLabel) {
570
            $this->classnameLabel = substr($this->getClass(), strrpos($this->getClass(), '\\') + 1);
571
        }
572
573
        $this->configure();
574
    }
575
576
    public function update(object $object): object
577
    {
578
        $this->preUpdate($object);
579
        foreach ($this->extensions as $extension) {
580
            $extension->preUpdate($this, $object);
581
        }
582
583
        $result = $this->getModelManager()->update($object);
584
        // BC compatibility
585
        if (null !== $result) {
586
            $object = $result;
587
        }
588
589
        $this->postUpdate($object);
590
        foreach ($this->extensions as $extension) {
591
            $extension->postUpdate($this, $object);
592
        }
593
594
        return $object;
595
    }
596
597
    public function create(object $object): object
598
    {
599
        $this->prePersist($object);
600
        foreach ($this->extensions as $extension) {
601
            $extension->prePersist($this, $object);
602
        }
603
604
        $result = $this->getModelManager()->create($object);
605
        // BC compatibility
606
        if (null !== $result) {
607
            $object = $result;
608
        }
609
610
        $this->postPersist($object);
611
        foreach ($this->extensions as $extension) {
612
            $extension->postPersist($this, $object);
613
        }
614
615
        $this->createObjectSecurity($object);
616
617
        return $object;
618
    }
619
620
    public function delete(object $object): void
621
    {
622
        $this->preRemove($object);
623
        foreach ($this->extensions as $extension) {
624
            $extension->preRemove($this, $object);
625
        }
626
627
        $this->getSecurityHandler()->deleteObjectSecurity($this, $object);
628
        $this->getModelManager()->delete($object);
629
630
        $this->postRemove($object);
631
        foreach ($this->extensions as $extension) {
632
            $extension->postRemove($this, $object);
633
        }
634
    }
635
636
    public function preValidate(object $object): void
637
    {
638
    }
639
640
    public function preUpdate(object $object): void
641
    {
642
    }
643
644
    public function postUpdate(object $object): void
645
    {
646
    }
647
648
    public function prePersist(object $object): void
649
    {
650
    }
651
652
    public function postPersist(object $object): void
653
    {
654
    }
655
656
    public function preRemove(object $object): void
657
    {
658
    }
659
660
    public function postRemove(object $object): void
661
    {
662
    }
663
664
    public function preBatchAction(string $actionName, ProxyQueryInterface $query, array &$idx, bool $allElements = false): void
665
    {
666
    }
667
668
    public function getFilterParameters(): array
669
    {
670
        $parameters = [];
671
672
        // build the values array
673
        if ($this->hasRequest()) {
674
            $filters = $this->request->query->get('filter', []);
675
            if (isset($filters['_page'])) {
676
                $filters['_page'] = (int) $filters['_page'];
677
            }
678
            if (isset($filters['_per_page'])) {
679
                $filters['_per_page'] = (int) $filters['_per_page'];
680
            }
681
682
            // if filter persistence is configured
683
            if (null !== $this->filterPersister) {
684
                // if reset filters is asked, remove from storage
685
                if ('reset' === $this->request->query->get('filters')) {
686
                    $this->filterPersister->reset($this->getCode());
687
                }
688
689
                // if no filters, fetch from storage
690
                // otherwise save to storage
691
                if (empty($filters)) {
692
                    $filters = $this->filterPersister->get($this->getCode());
693
                } else {
694
                    $this->filterPersister->set($this->getCode(), $filters);
695
                }
696
            }
697
698
            $parameters = array_merge(
699
                $this->getModelManager()->getDefaultSortValues($this->getClass()),
700
                $this->getDefaultSortValues(),
701
                $this->getDefaultFilterValues(),
702
                $filters
703
            );
704
705
            if (!isset($parameters['_per_page']) || !$this->determinedPerPageValue($parameters['_per_page'])) {
706
                $parameters['_per_page'] = $this->getMaxPerPage();
707
            }
708
709
            // always force the parent value
710
            if ($this->isChild() && $this->getParentAssociationMapping()) {
711
                $name = str_replace('.', '__', $this->getParentAssociationMapping());
712
                $parameters[$name] = ['value' => $this->request->get($this->getParent()->getIdParameter())];
713
            }
714
        }
715
716
        return $parameters;
717
    }
718
719
    /**
720
     * Returns the name of the parent related field, so the field can be use to set the default
721
     * value (ie the parent object) or to filter the object.
722
     *
723
     * @throws \InvalidArgumentException
724
     */
725
    public function getParentAssociationMapping(): ?string
726
    {
727
        if ($this->isChild()) {
728
            $parent = $this->getParent()->getCode();
729
730
            if (\array_key_exists($parent, $this->parentAssociationMapping)) {
731
                return $this->parentAssociationMapping[$parent];
732
            }
733
734
            throw new \InvalidArgumentException(sprintf(
735
                'There\'s no association between %s and %s.',
736
                $this->getCode(),
737
                $this->getParent()->getCode()
738
            ));
739
        }
740
741
        return null;
742
    }
743
744
    final public function addParentAssociationMapping(string $code, string $value): void
745
    {
746
        $this->parentAssociationMapping[$code] = $value;
747
    }
748
749
    /**
750
     * Returns the baseRoutePattern used to generate the routing information.
751
     *
752
     * @throws \RuntimeException
753
     *
754
     * @return string the baseRoutePattern used to generate the routing information
755
     */
756
    public function getBaseRoutePattern(): string
757
    {
758
        if (null !== $this->cachedBaseRoutePattern) {
759
            return $this->cachedBaseRoutePattern;
760
        }
761
762
        if ($this->isChild()) { // the admin class is a child, prefix it with the parent route pattern
763
            $baseRoutePattern = $this->baseRoutePattern;
764
            if (!$this->baseRoutePattern) {
765
                preg_match(self::CLASS_REGEX, $this->class, $matches);
766
767
                if (!$matches) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $matches of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
768
                    throw new \RuntimeException(sprintf(
769
                        'Please define a default `baseRoutePattern` value for the admin class `%s`',
770
                        static::class
771
                    ));
772
                }
773
                $baseRoutePattern = $this->urlize($matches[5], '-');
774
            }
775
776
            $this->cachedBaseRoutePattern = sprintf(
777
                '%s/%s/%s',
778
                $this->getParent()->getBaseRoutePattern(),
779
                $this->getParent()->getRouterIdParameter(),
780
                $baseRoutePattern
781
            );
782
        } elseif ($this->baseRoutePattern) {
783
            $this->cachedBaseRoutePattern = $this->baseRoutePattern;
784
        } else {
785
            preg_match(self::CLASS_REGEX, $this->class, $matches);
786
787
            if (!$matches) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $matches of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
788
                throw new \RuntimeException(sprintf(
789
                    'Please define a default `baseRoutePattern` value for the admin class `%s`',
790
                    static::class
791
                ));
792
            }
793
794
            $this->cachedBaseRoutePattern = sprintf(
795
                '/%s%s/%s',
796
                empty($matches[1]) ? '' : $this->urlize($matches[1], '-').'/',
797
                $this->urlize($matches[3], '-'),
798
                $this->urlize($matches[5], '-')
799
            );
800
        }
801
802
        return $this->cachedBaseRoutePattern;
803
    }
804
805
    /**
806
     * Returns the baseRouteName used to generate the routing information.
807
     *
808
     * @throws \RuntimeException
809
     *
810
     * @return string the baseRouteName used to generate the routing information
811
     */
812
    public function getBaseRouteName(): string
813
    {
814
        if (null !== $this->cachedBaseRouteName) {
815
            return $this->cachedBaseRouteName;
816
        }
817
818
        if ($this->isChild()) { // the admin class is a child, prefix it with the parent route name
819
            $baseRouteName = $this->baseRouteName;
820
            if (!$this->baseRouteName) {
821
                preg_match(self::CLASS_REGEX, $this->class, $matches);
822
823
                if (!$matches) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $matches of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
824
                    throw new \RuntimeException(sprintf(
825
                        'Cannot automatically determine base route name,'
826
                        .' please define a default `baseRouteName` value for the admin class `%s`',
827
                        static::class
828
                    ));
829
                }
830
                $baseRouteName = $this->urlize($matches[5]);
831
            }
832
833
            $this->cachedBaseRouteName = sprintf(
834
                '%s_%s',
835
                $this->getParent()->getBaseRouteName(),
836
                $baseRouteName
837
            );
838
        } elseif ($this->baseRouteName) {
839
            $this->cachedBaseRouteName = $this->baseRouteName;
840
        } else {
841
            preg_match(self::CLASS_REGEX, $this->class, $matches);
842
843
            if (!$matches) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $matches of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
844
                throw new \RuntimeException(sprintf(
845
                    'Cannot automatically determine base route name,'
846
                    .' please define a default `baseRouteName` value for the admin class `%s`',
847
                    static::class
848
                ));
849
            }
850
851
            $this->cachedBaseRouteName = sprintf(
852
                'admin_%s%s_%s',
853
                empty($matches[1]) ? '' : $this->urlize($matches[1]).'_',
854
                $this->urlize($matches[3]),
855
                $this->urlize($matches[5])
856
            );
857
        }
858
859
        return $this->cachedBaseRouteName;
860
    }
861
862
    public function getClass(): string
863
    {
864
        if ($this->hasActiveSubClass()) {
865
            if ($this->hasParentFieldDescription()) {
866
                throw new \RuntimeException('Feature not implemented: an embedded admin cannot have subclass');
867
            }
868
869
            $subClass = $this->getRequest()->query->get('subclass');
870
871
            if (!$this->hasSubClass($subClass)) {
872
                throw new \RuntimeException(sprintf('Subclass "%s" is not defined.', $subClass));
873
            }
874
875
            return $this->getSubClass($subClass);
876
        }
877
878
        // see https://github.com/sonata-project/SonataCoreBundle/commit/247eeb0a7ca7211142e101754769d70bc402a5b4
879
        if ($this->subject && \is_object($this->subject)) {
880
            return ClassUtils::getClass($this->subject);
881
        }
882
883
        return $this->class;
884
    }
885
886
    public function getSubClasses(): array
887
    {
888
        return $this->subClasses;
889
    }
890
891
    public function setSubClasses(array $subClasses): void
892
    {
893
        $this->subClasses = $subClasses;
894
    }
895
896
    public function hasSubClass(string $name): bool
897
    {
898
        return isset($this->subClasses[$name]);
899
    }
900
901
    public function hasActiveSubClass(): bool
902
    {
903
        if (\count($this->subClasses) > 0 && $this->request) {
904
            return null !== $this->getRequest()->query->get('subclass');
905
        }
906
907
        return false;
908
    }
909
910
    public function getActiveSubClass(): string
911
    {
912
        if (!$this->hasActiveSubClass()) {
913
            throw new \LogicException(sprintf(
914
                'Admin "%s" has no active subclass.',
915
                static::class
916
            ));
917
        }
918
919
        return $this->getSubClass($this->getActiveSubclassCode());
920
    }
921
922
    public function getActiveSubclassCode(): string
923
    {
924
        if (!$this->hasActiveSubClass()) {
925
            throw new \LogicException(sprintf(
926
                'Admin "%s" has no active subclass.',
927
                static::class
928
            ));
929
        }
930
931
        $subClass = $this->getRequest()->query->get('subclass');
932
933
        if (!$this->hasSubClass($subClass)) {
934
            throw new \LogicException(sprintf(
935
                'Admin "%s" has no active subclass.',
936
                static::class
937
            ));
938
        }
939
940
        return $subClass;
941
    }
942
943
    public function getBatchActions(): array
944
    {
945
        $actions = [];
946
947
        if ($this->hasRoute('delete') && $this->hasAccess('delete')) {
948
            $actions['delete'] = [
949
                'label' => 'action_delete',
950
                'translation_domain' => 'SonataAdminBundle',
951
                'ask_confirmation' => true, // by default always true
952
            ];
953
        }
954
955
        $actions = $this->configureBatchActions($actions);
956
957
        foreach ($this->getExtensions() as $extension) {
958
            $actions = $extension->configureBatchActions($this, $actions);
959
        }
960
961
        foreach ($actions  as $name => &$action) {
962
            if (!\array_key_exists('label', $action)) {
963
                $action['label'] = $this->getTranslationLabel($name, 'batch', 'label');
964
            }
965
966
            if (!\array_key_exists('translation_domain', $action)) {
967
                $action['translation_domain'] = $this->getTranslationDomain();
968
            }
969
        }
970
971
        return $actions;
972
    }
973
974
    public function getRoutes(): RouteCollectionInterface
975
    {
976
        $this->buildRoutes();
977
978
        return $this->routes;
979
    }
980
981
    public function getRouterIdParameter(): string
982
    {
983
        return sprintf('{%s}', $this->getIdParameter());
984
    }
985
986
    public function getIdParameter(): string
987
    {
988
        $parameter = 'id';
989
990
        for ($i = 0; $i < $this->getChildDepth(); ++$i) {
991
            $parameter = sprintf('child%s', ucfirst($parameter));
992
        }
993
994
        return $parameter;
995
    }
996
997
    public function hasRoute(string $name): bool
998
    {
999
        if (!$this->routeGenerator) {
1000
            throw new \RuntimeException('RouteGenerator cannot be null');
1001
        }
1002
1003
        return $this->routeGenerator->hasAdminRoute($this, $name);
1004
    }
1005
1006
    public function isCurrentRoute(string $name, ?string $adminCode = null): bool
1007
    {
1008
        if (!$this->hasRequest()) {
1009
            return false;
1010
        }
1011
1012
        $request = $this->getRequest();
1013
        $route = $request->get('_route');
1014
1015
        if ($adminCode) {
1016
            $pool = $this->getConfigurationPool();
1017
1018
            if ($pool->hasAdminByAdminCode($adminCode)) {
1019
                $admin = $pool->getAdminByAdminCode($adminCode);
1020
            } else {
1021
                return false;
1022
            }
1023
        } else {
1024
            $admin = $this;
1025
        }
1026
1027
        return sprintf('%s_%s', $admin->getBaseRouteName(), $name) === $route;
1028
    }
1029
1030
    public function generateObjectUrl(string $name, object $object, array $parameters = [], int $referenceType = RoutingUrlGeneratorInterface::ABSOLUTE_PATH): string
1031
    {
1032
        $parameters['id'] = $this->getUrlSafeIdentifier($object);
1033
1034
        return $this->generateUrl($name, $parameters, $referenceType);
1035
    }
1036
1037
    public function generateUrl(string $name, array $parameters = [], int $referenceType = RoutingUrlGeneratorInterface::ABSOLUTE_PATH): string
1038
    {
1039
        return $this->routeGenerator->generateUrl($this, $name, $parameters, $referenceType);
1040
    }
1041
1042
    public function generateMenuUrl(string $name, array $parameters = [], int $referenceType = RoutingUrlGeneratorInterface::ABSOLUTE_PATH): array
1043
    {
1044
        return $this->routeGenerator->generateMenuUrl($this, $name, $parameters, $referenceType);
1045
    }
1046
1047
    final public function setTemplateRegistry(MutableTemplateRegistryInterface $templateRegistry): void
1048
    {
1049
        $this->templateRegistry = $templateRegistry;
1050
    }
1051
1052
    /**
1053
     * @param array<string, string> $templates
1054
     */
1055
    public function setTemplates(array $templates): void
1056
    {
1057
        $this->getTemplateRegistry()->setTemplates($templates);
1058
    }
1059
1060
    /**
1061
     * {@inheritdoc}
1062
     */
1063
    public function setTemplate(string $name, string $template): void
1064
    {
1065
        $this->getTemplateRegistry()->setTemplate($name, $template);
1066
    }
1067
1068
    public function getNewInstance(): object
1069
    {
1070
        $object = $this->getModelManager()->getModelInstance($this->getClass());
1071
1072
        $this->appendParentObject($object);
1073
1074
        foreach ($this->getExtensions() as $extension) {
1075
            $extension->alterNewInstance($this, $object);
1076
        }
1077
1078
        return $object;
1079
    }
1080
1081
    public function getFormBuilder(): FormBuilderInterface
1082
    {
1083
        $this->formOptions['data_class'] = $this->getClass();
1084
1085
        $formBuilder = $this->getFormContractor()->getFormBuilder(
1086
            $this->getUniqid(),
1087
            $this->formOptions
1088
        );
1089
1090
        $this->defineFormBuilder($formBuilder);
1091
1092
        return $formBuilder;
1093
    }
1094
1095
    /**
1096
     * This method is being called by the main admin class and the child class,
1097
     * the getFormBuilder is only call by the main admin class.
1098
     */
1099
    public function defineFormBuilder(FormBuilderInterface $formBuilder): void
1100
    {
1101
        if (!$this->hasSubject()) {
1102
            throw new \LogicException(sprintf(
1103
                'Admin "%s" has no subject.',
1104
                static::class
1105
            ));
1106
        }
1107
1108
        $mapper = new FormMapper($this->getFormContractor(), $formBuilder, $this);
1109
1110
        $this->configureFormFields($mapper);
1111
1112
        foreach ($this->getExtensions() as $extension) {
1113
            $extension->configureFormFields($mapper);
1114
        }
1115
1116
        $this->attachInlineValidator();
1117
    }
1118
1119
    public function attachAdminClass(FieldDescriptionInterface $fieldDescription): void
1120
    {
1121
        $pool = $this->getConfigurationPool();
1122
1123
        $adminCode = $fieldDescription->getOption('admin_code');
1124
1125
        if (null !== $adminCode) {
1126
            if (!$pool->hasAdminByAdminCode($adminCode)) {
1127
                return;
1128
            }
1129
1130
            $admin = $pool->getAdminByAdminCode($adminCode);
1131
        } else {
1132
            $targetModel = $fieldDescription->getTargetModel();
1133
1134
            if (!$pool->hasAdminByClass($targetModel)) {
1135
                return;
1136
            }
1137
1138
            $admin = $pool->getAdminByClass($targetModel);
1139
        }
1140
1141
        if ($this->hasRequest()) {
1142
            $admin->setRequest($this->getRequest());
1143
        }
1144
1145
        $fieldDescription->setAssociationAdmin($admin);
1146
    }
1147
1148
    public function getObject($id): ?object
1149
    {
1150
        $object = $this->getModelManager()->find($this->getClass(), $id);
1151
        foreach ($this->getExtensions() as $extension) {
1152
            $extension->alterObject($this, $object);
0 ignored issues
show
Documentation introduced by
$object is of type object|null, but the function expects a object<Sonata\AdminBundle\Admin\object>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1153
        }
1154
1155
        return $object;
1156
    }
1157
1158
    public function getForm(): ?FormInterface
1159
    {
1160
        $this->buildForm();
1161
1162
        return $this->form;
1163
    }
1164
1165
    public function getList(): ?FieldDescriptionCollection
1166
    {
1167
        $this->buildList();
1168
1169
        return $this->list;
1170
    }
1171
1172
    final public function createQuery(): ProxyQueryInterface
1173
    {
1174
        $query = $this->getModelManager()->createQuery($this->getClass());
1175
1176
        $query = $this->configureQuery($query);
1177
        foreach ($this->extensions as $extension) {
1178
            $extension->configureQuery($this, $query);
1179
        }
1180
1181
        return $query;
1182
    }
1183
1184
    public function getDatagrid(): DatagridInterface
1185
    {
1186
        $this->buildDatagrid();
1187
1188
        return $this->datagrid;
1189
    }
1190
1191
    public function buildTabMenu(string $action, ?AdminInterface $childAdmin = null): ItemInterface
1192
    {
1193
        if ($this->loaded['tab_menu']) {
1194
            return $this->menu;
1195
        }
1196
1197
        $this->loaded['tab_menu'] = true;
1198
1199
        $menu = $this->menuFactory->createItem('root');
1200
        $menu->setChildrenAttribute('class', 'nav navbar-nav');
1201
        $menu->setExtra('translation_domain', $this->translationDomain);
1202
1203
        // Prevents BC break with KnpMenuBundle v1.x
1204
        if (method_exists($menu, 'setCurrentUri')) {
1205
            $menu->setCurrentUri($this->getRequest()->getBaseUrl().$this->getRequest()->getPathInfo());
0 ignored issues
show
Bug introduced by
The method setCurrentUri() does not exist on Knp\Menu\ItemInterface. Did you maybe mean setCurrent()?

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

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

Loading history...
1206
        }
1207
1208
        $this->configureTabMenu($menu, $action, $childAdmin);
1209
1210
        foreach ($this->getExtensions() as $extension) {
1211
            $extension->configureTabMenu($this, $menu, $action, $childAdmin);
1212
        }
1213
1214
        $this->menu = $menu;
1215
1216
        return $this->menu;
1217
    }
1218
1219
    public function getSideMenu(string $action, ?AdminInterface $childAdmin = null): ItemInterface
1220
    {
1221
        if ($this->isChild()) {
1222
            return $this->getParent()->getSideMenu($action, $this);
1223
        }
1224
1225
        $this->buildTabMenu($action, $childAdmin);
1226
1227
        return $this->menu;
1228
    }
1229
1230
    public function getRootCode(): string
1231
    {
1232
        return $this->getRoot()->getCode();
1233
    }
1234
1235
    public function getRoot(): AdminInterface
1236
    {
1237
        if (!$this->hasParentFieldDescription()) {
1238
            return $this;
1239
        }
1240
1241
        return $this->getParentFieldDescription()->getAdmin()->getRoot();
1242
    }
1243
1244
    public function setBaseControllerName(string $baseControllerName): void
1245
    {
1246
        $this->baseControllerName = $baseControllerName;
1247
    }
1248
1249
    public function getBaseControllerName(): string
1250
    {
1251
        return $this->baseControllerName;
1252
    }
1253
1254
    public function setLabel(?string $label): void
1255
    {
1256
        $this->label = $label;
1257
    }
1258
1259
    public function getLabel(): ?string
1260
    {
1261
        return $this->label;
1262
    }
1263
1264
    public function setFilterPersister(?FilterPersisterInterface $filterPersister = null): void
1265
    {
1266
        $this->filterPersister = $filterPersister;
1267
    }
1268
1269
    public function getMaxPerPage(): int
1270
    {
1271
        $sortValues = $this->getModelManager()->getDefaultSortValues($this->class);
1272
1273
        return $sortValues['_per_page'] ?? 25;
1274
    }
1275
1276
    public function setMaxPageLinks(int $maxPageLinks): void
1277
    {
1278
        $this->maxPageLinks = $maxPageLinks;
1279
    }
1280
1281
    public function getMaxPageLinks(): int
1282
    {
1283
        return $this->maxPageLinks;
1284
    }
1285
1286
    public function getFormGroups(): array
1287
    {
1288
        return $this->formGroups;
1289
    }
1290
1291
    public function setFormGroups(array $formGroups): void
1292
    {
1293
        $this->formGroups = $formGroups;
1294
    }
1295
1296
    public function removeFieldFromFormGroup(string $key): void
1297
    {
1298
        foreach ($this->formGroups as $name => $formGroup) {
1299
            unset($this->formGroups[$name]['fields'][$key]);
1300
1301
            if (empty($this->formGroups[$name]['fields'])) {
1302
                unset($this->formGroups[$name]);
1303
            }
1304
        }
1305
    }
1306
1307
    public function reorderFormGroup(string $group, array $keys): void
1308
    {
1309
        $formGroups = $this->getFormGroups();
1310
        $formGroups[$group]['fields'] = array_merge(array_flip($keys), $formGroups[$group]['fields']);
1311
        $this->setFormGroups($formGroups);
1312
    }
1313
1314
    public function getFormTabs(): array
1315
    {
1316
        return $this->formTabs;
1317
    }
1318
1319
    public function setFormTabs(array $formTabs): void
1320
    {
1321
        $this->formTabs = $formTabs;
1322
    }
1323
1324
    public function getShowTabs(): array
1325
    {
1326
        return $this->showTabs;
1327
    }
1328
1329
    public function setShowTabs(array $showTabs): void
1330
    {
1331
        $this->showTabs = $showTabs;
1332
    }
1333
1334
    public function getShowGroups(): array
1335
    {
1336
        return $this->showGroups;
1337
    }
1338
1339
    public function setShowGroups(array $showGroups): void
1340
    {
1341
        $this->showGroups = $showGroups;
1342
    }
1343
1344
    public function reorderShowGroup(string $group, array $keys): void
1345
    {
1346
        $showGroups = $this->getShowGroups();
1347
        $showGroups[$group]['fields'] = array_merge(array_flip($keys), $showGroups[$group]['fields']);
1348
        $this->setShowGroups($showGroups);
1349
    }
1350
1351
    public function setParentFieldDescription(FieldDescriptionInterface $parentFieldDescription): void
1352
    {
1353
        $this->parentFieldDescription = $parentFieldDescription;
1354
    }
1355
1356
    public function getParentFieldDescription(): FieldDescriptionInterface
1357
    {
1358
        if (!$this->hasParentFieldDescription()) {
1359
            throw new \LogicException(sprintf(
1360
                'Admin "%s" has no parent field description.',
1361
                static::class
1362
            ));
1363
        }
1364
1365
        return $this->parentFieldDescription;
1366
    }
1367
1368
    public function hasParentFieldDescription(): bool
1369
    {
1370
        return $this->parentFieldDescription instanceof FieldDescriptionInterface;
1371
    }
1372
1373
    public function setSubject(?object $subject): void
1374
    {
1375
        if (\is_object($subject) && !is_a($subject, $this->getClass(), true)) {
1376
            throw new \LogicException(sprintf(
1377
                'Admin "%s" does not allow this subject: %s, use the one register with this admin class %s',
1378
                static::class,
1379
                \get_class($subject),
1380
                $this->getClass()
1381
            ));
1382
        }
1383
1384
        $this->subject = $subject;
1385
    }
1386
1387
    public function getSubject(): object
1388
    {
1389
        if (!$this->hasSubject()) {
1390
            throw new \LogicException(sprintf(
1391
                'Admin "%s" has no subject.',
1392
                static::class
1393
            ));
1394
        }
1395
1396
        return $this->subject;
1397
    }
1398
1399
    public function hasSubject(): bool
1400
    {
1401
        if (null === $this->subject && $this->hasRequest() && !$this->hasParentFieldDescription()) {
1402
            $id = $this->request->get($this->getIdParameter());
1403
1404
            if (null !== $id) {
1405
                $this->subject = $this->getObject($id);
1406
            }
1407
        }
1408
1409
        return null !== $this->subject;
1410
    }
1411
1412
    public function getFormFieldDescriptions(): array
1413
    {
1414
        $this->buildForm();
1415
1416
        return $this->formFieldDescriptions;
1417
    }
1418
1419
    public function getFormFieldDescription(string $name): FieldDescriptionInterface
1420
    {
1421
        $this->buildForm();
1422
1423
        if (!$this->hasFormFieldDescription($name)) {
1424
            throw new \LogicException(sprintf(
1425
                'Admin "%s" has no form field description for the field %s.',
1426
                static::class,
1427
                $name
1428
            ));
1429
        }
1430
1431
        return $this->formFieldDescriptions[$name];
1432
    }
1433
1434
    /**
1435
     * Returns true if the admin has a FieldDescription with the given $name.
1436
     */
1437
    public function hasFormFieldDescription(string $name): bool
1438
    {
1439
        $this->buildForm();
1440
1441
        return \array_key_exists($name, $this->formFieldDescriptions) ? true : false;
1442
    }
1443
1444
    public function addFormFieldDescription(string $name, FieldDescriptionInterface $fieldDescription): void
1445
    {
1446
        $this->formFieldDescriptions[$name] = $fieldDescription;
1447
    }
1448
1449
    /**
1450
     * remove a FieldDescription.
1451
     */
1452
    public function removeFormFieldDescription(string $name): void
1453
    {
1454
        unset($this->formFieldDescriptions[$name]);
1455
    }
1456
1457
    /**
1458
     * build and return the collection of form FieldDescription.
1459
     *
1460
     * @return FieldDescriptionInterface[] collection of form FieldDescription
1461
     */
1462
    public function getShowFieldDescriptions(): array
1463
    {
1464
        $this->buildShow();
1465
1466
        return $this->showFieldDescriptions;
1467
    }
1468
1469
    /**
1470
     * Returns the form FieldDescription with the given $name.
1471
     */
1472
    public function getShowFieldDescription(string $name): FieldDescriptionInterface
1473
    {
1474
        $this->buildShow();
1475
1476
        if (!$this->hasShowFieldDescription($name)) {
1477
            throw new \LogicException(sprintf(
1478
                'Admin "%s" has no show field description for the field %s.',
1479
                static::class,
1480
                $name
1481
            ));
1482
        }
1483
1484
        return $this->showFieldDescriptions[$name];
1485
    }
1486
1487
    public function hasShowFieldDescription(string $name): bool
1488
    {
1489
        $this->buildShow();
1490
1491
        return \array_key_exists($name, $this->showFieldDescriptions);
1492
    }
1493
1494
    public function addShowFieldDescription(string $name, FieldDescriptionInterface $fieldDescription): void
1495
    {
1496
        $this->showFieldDescriptions[$name] = $fieldDescription;
1497
    }
1498
1499
    public function removeShowFieldDescription(string $name): void
1500
    {
1501
        unset($this->showFieldDescriptions[$name]);
1502
    }
1503
1504
    public function getListFieldDescriptions(): array
1505
    {
1506
        $this->buildList();
1507
1508
        return $this->listFieldDescriptions;
1509
    }
1510
1511
    public function getListFieldDescription(string $name): FieldDescriptionInterface
1512
    {
1513
        $this->buildList();
1514
1515
        if (!$this->hasListFieldDescription($name)) {
1516
            throw new \LogicException(sprintf(
1517
                'Admin "%s" has no list field description for %s.',
1518
                static::class,
1519
                $name
1520
            ));
1521
        }
1522
1523
        return $this->listFieldDescriptions[$name];
1524
    }
1525
1526
    public function hasListFieldDescription(string $name): bool
1527
    {
1528
        $this->buildList();
1529
1530
        return \array_key_exists($name, $this->listFieldDescriptions) ? true : false;
1531
    }
1532
1533
    public function addListFieldDescription(string $name, FieldDescriptionInterface $fieldDescription): void
1534
    {
1535
        $this->listFieldDescriptions[$name] = $fieldDescription;
1536
    }
1537
1538
    public function removeListFieldDescription(string $name): void
1539
    {
1540
        unset($this->listFieldDescriptions[$name]);
1541
    }
1542
1543
    public function getFilterFieldDescription(string $name): FieldDescriptionInterface
1544
    {
1545
        $this->buildDatagrid();
1546
1547
        if (!$this->hasFilterFieldDescription($name)) {
1548
            throw new \LogicException(sprintf(
1549
                'Admin "%s" has no filter field description for the field %s.',
1550
                static::class,
1551
                $name
1552
            ));
1553
        }
1554
1555
        return $this->filterFieldDescriptions[$name];
1556
    }
1557
1558
    public function hasFilterFieldDescription(string $name): bool
1559
    {
1560
        $this->buildDatagrid();
1561
1562
        return \array_key_exists($name, $this->filterFieldDescriptions) ? true : false;
1563
    }
1564
1565
    public function addFilterFieldDescription(string $name, FieldDescriptionInterface $fieldDescription): void
1566
    {
1567
        $this->filterFieldDescriptions[$name] = $fieldDescription;
1568
    }
1569
1570
    public function removeFilterFieldDescription(string $name): void
1571
    {
1572
        unset($this->filterFieldDescriptions[$name]);
1573
    }
1574
1575
    public function getFilterFieldDescriptions(): array
1576
    {
1577
        $this->buildDatagrid();
1578
1579
        return $this->filterFieldDescriptions;
1580
    }
1581
1582
    public function addChild(AdminInterface $child, string $field): void
1583
    {
1584
        $parentAdmin = $this;
1585
        while ($parentAdmin->isChild() && $parentAdmin->getCode() !== $child->getCode()) {
1586
            $parentAdmin = $parentAdmin->getParent();
1587
        }
1588
1589
        if ($parentAdmin->getCode() === $child->getCode()) {
1590
            throw new \RuntimeException(sprintf(
1591
                'Circular reference detected! The child admin `%s` is already in the parent tree of the `%s` admin.',
1592
                $child->getCode(),
1593
                $this->getCode()
1594
            ));
1595
        }
1596
1597
        $this->children[$child->getCode()] = $child;
1598
1599
        $child->setParent($this);
0 ignored issues
show
Documentation introduced by
$this is of type this<Sonata\AdminBundle\Admin\AbstractAdmin>, but the function expects a object<self>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1600
        $child->addParentAssociationMapping($this->getCode(), $field);
1601
    }
1602
1603
    public function hasChild(string $code): bool
1604
    {
1605
        return isset($this->children[$code]);
1606
    }
1607
1608
    public function getChildren(): array
1609
    {
1610
        return $this->children;
1611
    }
1612
1613
    public function getChild(string $code): AdminInterface
1614
    {
1615
        if (!$this->hasChild($code)) {
1616
            throw new \LogicException(sprintf(
1617
                'Admin "%s" has no child for the code %s.',
1618
                static::class,
1619
                $code
1620
            ));
1621
        }
1622
1623
        return $this->children[$code];
1624
    }
1625
1626
    public function setParent(AdminInterface $parent): void
1627
    {
1628
        $this->parent = $parent;
1629
    }
1630
1631
    public function getParent(): AdminInterface
1632
    {
1633
        if (!$this->isChild()) {
1634
            throw new \LogicException(sprintf(
1635
                'Admin "%s" has no parent.',
1636
                static::class
1637
            ));
1638
        }
1639
1640
        return $this->parent;
1641
    }
1642
1643
    final public function getRootAncestor(): AdminInterface
1644
    {
1645
        $parent = $this;
1646
1647
        while ($parent->isChild()) {
1648
            $parent = $parent->getParent();
1649
        }
1650
1651
        return $parent;
1652
    }
1653
1654
    final public function getChildDepth(): int
1655
    {
1656
        $parent = $this;
1657
        $depth = 0;
1658
1659
        while ($parent->isChild()) {
1660
            $parent = $parent->getParent();
1661
            ++$depth;
1662
        }
1663
1664
        return $depth;
1665
    }
1666
1667
    final public function getCurrentLeafChildAdmin(): ?AdminInterface
1668
    {
1669
        $child = $this->getCurrentChildAdmin();
1670
1671
        if (null === $child) {
1672
            return null;
1673
        }
1674
1675
        for ($c = $child; null !== $c; $c = $child->getCurrentChildAdmin()) {
1676
            $child = $c;
1677
        }
1678
1679
        return $child;
1680
    }
1681
1682
    public function isChild(): bool
1683
    {
1684
        return $this->parent instanceof AdminInterface;
1685
    }
1686
1687
    /**
1688
     * Returns true if the admin has children, false otherwise.
1689
     */
1690
    public function hasChildren(): bool
1691
    {
1692
        return \count($this->children) > 0;
1693
    }
1694
1695
    public function setUniqid(string $uniqid): void
1696
    {
1697
        $this->uniqid = $uniqid;
1698
    }
1699
1700
    public function getUniqid(): string
1701
    {
1702
        if (!$this->uniqid) {
1703
            $this->uniqid = sprintf('s%s', uniqid());
1704
        }
1705
1706
        return $this->uniqid;
1707
    }
1708
1709
    /**
1710
     * {@inheritdoc}
1711
     */
1712
    public function getClassnameLabel(): string
1713
    {
1714
        return $this->classnameLabel;
1715
    }
1716
1717
    public function getPersistentParameters(): array
1718
    {
1719
        $parameters = [];
1720
1721
        foreach ($this->getExtensions() as $extension) {
1722
            $params = $extension->getPersistentParameters($this);
1723
1724
            $parameters = array_merge($parameters, $params);
1725
        }
1726
1727
        return $parameters;
1728
    }
1729
1730
    /**
1731
     * {@inheritdoc}
1732
     */
1733
    public function getPersistentParameter(string $name)
1734
    {
1735
        $parameters = $this->getPersistentParameters();
1736
1737
        return $parameters[$name] ?? null;
1738
    }
1739
1740
    public function setCurrentChild(bool $currentChild): void
1741
    {
1742
        $this->currentChild = $currentChild;
1743
    }
1744
1745
    public function isCurrentChild(): bool
1746
    {
1747
        return $this->currentChild;
1748
    }
1749
1750
    /**
1751
     * Returns the current child admin instance.
1752
     *
1753
     * @return AdminInterface|null the current child admin instance
1754
     */
1755
    public function getCurrentChildAdmin(): ?AdminInterface
1756
    {
1757
        foreach ($this->children as $children) {
1758
            if ($children->isCurrentChild()) {
1759
                return $children;
1760
            }
1761
        }
1762
1763
        return null;
1764
    }
1765
1766
    public function setTranslationDomain(string $translationDomain): void
1767
    {
1768
        $this->translationDomain = $translationDomain;
1769
    }
1770
1771
    public function getTranslationDomain(): string
1772
    {
1773
        return $this->translationDomain;
1774
    }
1775
1776
    /**
1777
     * {@inheritdoc}
1778
     *
1779
     * NEXT_MAJOR: remove this method
1780
     *
1781
     * @deprecated since sonata-project/admin-bundle 3.9, to be removed with 4.0
1782
     */
1783
    public function setTranslator(TranslatorInterface $translator): void
1784
    {
1785
        $args = \func_get_args();
1786
        if (isset($args[1]) && $args[1]) {
1787
            @trigger_error(sprintf(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1788
                'The %s method is deprecated since version 3.9 and will be removed in 4.0.',
1789
                __METHOD__
1790
            ), E_USER_DEPRECATED);
1791
        }
1792
1793
        $this->translator = $translator;
0 ignored issues
show
Deprecated Code introduced by
The property Sonata\AdminBundle\Admin...tractAdmin::$translator has been deprecated with message: since sonata-project/admin-bundle 3.9, to be removed with 4.0

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

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

Loading history...
1794
    }
1795
1796
    public function getTranslationLabel(string $label, string $context = '', string $type = ''): string
1797
    {
1798
        return $this->getLabelTranslatorStrategy()->getLabel($label, $context, $type);
1799
    }
1800
1801
    public function setRequest(Request $request): void
0 ignored issues
show
Bug introduced by
You have injected the Request via parameter $request. This is generally not recommended as there might be multiple instances during a request cycle (f.e. when using sub-requests). Instead, it is recommended to inject the RequestStack and retrieve the current request each time you need it via getCurrentRequest().
Loading history...
1802
    {
1803
        $this->request = $request;
1804
1805
        foreach ($this->getChildren() as $children) {
1806
            $children->setRequest($request);
1807
        }
1808
    }
1809
1810
    public function getRequest(): Request
1811
    {
1812
        if (!$this->request) {
1813
            throw new \LogicException('The Request object has not been set');
1814
        }
1815
1816
        return $this->request;
1817
    }
1818
1819
    public function hasRequest(): bool
1820
    {
1821
        return null !== $this->request;
1822
    }
1823
1824
    public function setFormContractor(FormContractorInterface $formBuilder): void
1825
    {
1826
        $this->formContractor = $formBuilder;
1827
    }
1828
1829
    public function getFormContractor(): ?FormContractorInterface
1830
    {
1831
        return $this->formContractor;
1832
    }
1833
1834
    public function setDatagridBuilder(DatagridBuilderInterface $datagridBuilder): void
1835
    {
1836
        $this->datagridBuilder = $datagridBuilder;
1837
    }
1838
1839
    public function getDatagridBuilder(): ?DatagridBuilderInterface
1840
    {
1841
        return $this->datagridBuilder;
1842
    }
1843
1844
    public function setListBuilder(ListBuilderInterface $listBuilder): void
1845
    {
1846
        $this->listBuilder = $listBuilder;
1847
    }
1848
1849
    public function getListBuilder(): ?ListBuilderInterface
1850
    {
1851
        return $this->listBuilder;
1852
    }
1853
1854
    public function setShowBuilder(?ShowBuilderInterface $showBuilder): void
1855
    {
1856
        $this->showBuilder = $showBuilder;
1857
    }
1858
1859
    public function getShowBuilder(): ?ShowBuilderInterface
1860
    {
1861
        return $this->showBuilder;
1862
    }
1863
1864
    public function setConfigurationPool(Pool $configurationPool): void
1865
    {
1866
        $this->configurationPool = $configurationPool;
1867
    }
1868
1869
    public function getConfigurationPool(): ?Pool
1870
    {
1871
        return $this->configurationPool;
1872
    }
1873
1874
    public function setRouteGenerator(RouteGeneratorInterface $routeGenerator): void
1875
    {
1876
        $this->routeGenerator = $routeGenerator;
1877
    }
1878
1879
    public function getRouteGenerator(): ?RouteGeneratorInterface
1880
    {
1881
        return $this->routeGenerator;
1882
    }
1883
1884
    public function getCode(): string
1885
    {
1886
        return $this->code;
1887
    }
1888
1889
    public function getBaseCodeRoute(): string
1890
    {
1891
        if ($this->isChild()) {
1892
            return $this->getParent()->getBaseCodeRoute().'|'.$this->getCode();
1893
        }
1894
1895
        return $this->getCode();
1896
    }
1897
1898
    public function getModelManager(): ?ModelManagerInterface
1899
    {
1900
        return $this->modelManager;
1901
    }
1902
1903
    public function setModelManager(?ModelManagerInterface $modelManager): void
1904
    {
1905
        $this->modelManager = $modelManager;
1906
    }
1907
1908
    public function getManagerType(): ?string
1909
    {
1910
        return $this->managerType;
1911
    }
1912
1913
    public function setManagerType(?string $type): void
1914
    {
1915
        $this->managerType = $type;
1916
    }
1917
1918
    public function getObjectIdentifier()
1919
    {
1920
        return $this->getCode();
1921
    }
1922
1923
    /**
1924
     * Set the roles and permissions per role.
1925
     */
1926
    public function setSecurityInformation(array $information): void
1927
    {
1928
        $this->securityInformation = $information;
1929
    }
1930
1931
    public function getSecurityInformation(): array
1932
    {
1933
        return $this->securityInformation;
1934
    }
1935
1936
    /**
1937
     * Return the list of permissions the user should have in order to display the admin.
1938
     */
1939
    public function getPermissionsShow(string $context): array
1940
    {
1941
        switch ($context) {
1942
            case self::CONTEXT_DASHBOARD:
1943
            case self::CONTEXT_MENU:
1944
            default:
1945
                return ['LIST'];
1946
        }
1947
    }
1948
1949
    public function showIn(string $context): bool
1950
    {
1951
        return $this->isGranted($this->getPermissionsShow($context));
1952
    }
1953
1954
    public function createObjectSecurity(object $object): void
1955
    {
1956
        $this->getSecurityHandler()->createObjectSecurity($this, $object);
1957
    }
1958
1959
    public function setSecurityHandler(SecurityHandlerInterface $securityHandler): void
1960
    {
1961
        $this->securityHandler = $securityHandler;
1962
    }
1963
1964
    public function getSecurityHandler(): ?SecurityHandlerInterface
1965
    {
1966
        return $this->securityHandler;
1967
    }
1968
1969
    /**
1970
     * NEXT_MAJOR: Decide the type declaration for the $name argument, since it is
1971
     * passed as argument 1 for `SecurityHandlerInterface::isGranted()`, which
1972
     * accepts string and array.
1973
     */
1974
    public function isGranted($name, ?object $object = null): bool
1975
    {
1976
        $objectRef = $object ? sprintf('/%s#%s', spl_object_hash($object), $this->id($object)) : '';
1977
        $key = md5(json_encode($name).$objectRef);
1978
1979
        if (!\array_key_exists($key, $this->cacheIsGranted)) {
1980
            $this->cacheIsGranted[$key] = $this->securityHandler->isGranted($this, $name, $object ?: $this);
1981
        }
1982
1983
        return $this->cacheIsGranted[$key];
1984
    }
1985
1986
    /**
1987
     * NEXT_MAJOR: Decide the type declaration for the $model argument, since it is
1988
     * passed as argument 1 for `ModelManagerInterface::getUrlSafeIdentifier()`, which
1989
     * accepts null.
1990
     */
1991
    public function getUrlSafeIdentifier($model): ?string
1992
    {
1993
        return $this->getModelManager()->getUrlSafeIdentifier($model);
1994
    }
1995
1996
    /**
1997
     * NEXT_MAJOR: Decide the type declaration for the $model argument, since it is
1998
     * passed as argument 1 for `ModelManagerInterface::getNormalizedIdentifier()`, which
1999
     * accepts null.
2000
     */
2001
    public function getNormalizedIdentifier($model): ?string
2002
    {
2003
        return $this->getModelManager()->getNormalizedIdentifier($model);
2004
    }
2005
2006
    /**
2007
     * NEXT_MAJOR: Decide the type declaration for the $model argument, since it is
2008
     * passed as argument 1 for `ModelManagerInterface::getNormalizedIdentifier()`, which
2009
     * accepts null.
2010
     */
2011
    public function id($model): ?string
2012
    {
2013
        return $this->getNormalizedIdentifier($model);
2014
    }
2015
2016
    public function setValidator(ValidatorInterface $validator): void
2017
    {
2018
        $this->validator = $validator;
2019
    }
2020
2021
    public function getValidator(): ?ValidatorInterface
2022
    {
2023
        return $this->validator;
2024
    }
2025
2026
    public function getShow(): ?FieldDescriptionCollection
2027
    {
2028
        $this->buildShow();
2029
2030
        return $this->show;
2031
    }
2032
2033
    public function setFormTheme(array $formTheme): void
2034
    {
2035
        $this->formTheme = $formTheme;
2036
    }
2037
2038
    public function getFormTheme(): array
2039
    {
2040
        return $this->formTheme;
2041
    }
2042
2043
    public function setFilterTheme(array $filterTheme): void
2044
    {
2045
        $this->filterTheme = $filterTheme;
2046
    }
2047
2048
    public function getFilterTheme(): array
2049
    {
2050
        return $this->filterTheme;
2051
    }
2052
2053
    public function addExtension(AdminExtensionInterface $extension): void
2054
    {
2055
        $this->extensions[] = $extension;
2056
    }
2057
2058
    public function getExtensions(): array
2059
    {
2060
        return $this->extensions;
2061
    }
2062
2063
    public function setMenuFactory(FactoryInterface $menuFactory): void
2064
    {
2065
        $this->menuFactory = $menuFactory;
2066
    }
2067
2068
    public function getMenuFactory(): ?FactoryInterface
2069
    {
2070
        return $this->menuFactory;
2071
    }
2072
2073
    public function setRouteBuilder(RouteBuilderInterface $routeBuilder): void
2074
    {
2075
        $this->routeBuilder = $routeBuilder;
2076
    }
2077
2078
    public function getRouteBuilder(): ?RouteBuilderInterface
2079
    {
2080
        return $this->routeBuilder;
2081
    }
2082
2083
    /**
2084
     * NEXT_MAJOR: Decide the type declaration for the $object argument, since there
2085
     * are tests ensuring to accept null (`GetShortObjectDescriptionActionTest::testGetShortObjectDescriptionActionEmptyObjectIdAsJson()`).
2086
     */
2087
    public function toString($object): string
2088
    {
2089
        if (!\is_object($object)) {
2090
            return '';
2091
        }
2092
2093
        if (method_exists($object, '__toString') && null !== $object->__toString()) {
2094
            return (string) $object;
2095
        }
2096
2097
        return sprintf('%s:%s', ClassUtils::getClass($object), spl_object_hash($object));
2098
    }
2099
2100
    public function setLabelTranslatorStrategy(LabelTranslatorStrategyInterface $labelTranslatorStrategy): void
2101
    {
2102
        $this->labelTranslatorStrategy = $labelTranslatorStrategy;
2103
    }
2104
2105
    public function getLabelTranslatorStrategy(): ?LabelTranslatorStrategyInterface
2106
    {
2107
        return $this->labelTranslatorStrategy;
2108
    }
2109
2110
    public function supportsPreviewMode(): bool
2111
    {
2112
        return $this->supportsPreviewMode;
2113
    }
2114
2115
    /**
2116
     * Returns predefined per page options.
2117
     */
2118
    public function getPerPageOptions(): array
2119
    {
2120
        $perPageOptions = $this->getModelManager()->getDefaultPerPageOptions($this->class);
2121
        $perPageOptions[] = $this->getMaxPerPage();
2122
2123
        $perPageOptions = array_unique($perPageOptions);
2124
        sort($perPageOptions);
2125
2126
        return $perPageOptions;
2127
    }
2128
2129
    /**
2130
     * Set pager type.
2131
     */
2132
    public function setPagerType(string $pagerType): void
2133
    {
2134
        $this->pagerType = $pagerType;
2135
    }
2136
2137
    /**
2138
     * Get pager type.
2139
     */
2140
    public function getPagerType(): string
2141
    {
2142
        return $this->pagerType;
2143
    }
2144
2145
    /**
2146
     * Returns true if the per page value is allowed, false otherwise.
2147
     */
2148
    public function determinedPerPageValue(int $perPage): bool
2149
    {
2150
        return \in_array($perPage, $this->getPerPageOptions(), true);
2151
    }
2152
2153
    public function isAclEnabled(): bool
2154
    {
2155
        return $this->getSecurityHandler() instanceof AclSecurityHandlerInterface;
2156
    }
2157
2158
    /**
2159
     * NEXT_MAJOR: Decide the type declaration for the $object argument, since it is
2160
     * passed as argument 1 to `toString()` method, which currently accepts null.
2161
     */
2162
    public function getObjectMetadata($object): MetadataInterface
2163
    {
2164
        return new Metadata($this->toString($object));
2165
    }
2166
2167
    public function getListModes(): array
2168
    {
2169
        return $this->listModes;
2170
    }
2171
2172
    public function setListMode(string $mode): void
2173
    {
2174
        if (!$this->hasRequest()) {
2175
            throw new \RuntimeException(sprintf('No request attached to the current admin: %s', $this->getCode()));
2176
        }
2177
2178
        $this->getRequest()->getSession()->set(sprintf('%s.list_mode', $this->getCode()), $mode);
2179
    }
2180
2181
    public function getListMode(): string
2182
    {
2183
        if (!$this->hasRequest()) {
2184
            return 'list';
2185
        }
2186
2187
        return $this->getRequest()->getSession()->get(sprintf('%s.list_mode', $this->getCode()), 'list');
2188
    }
2189
2190
    public function getAccessMapping(): array
2191
    {
2192
        return $this->accessMapping;
2193
    }
2194
2195
    public function checkAccess(string $action, ?object $object = null): void
2196
    {
2197
        $access = $this->getAccess();
2198
2199
        if (!\array_key_exists($action, $access)) {
2200
            throw new \InvalidArgumentException(sprintf(
2201
                'Action "%s" could not be found in access mapping.'
2202
                .' Please make sure your action is defined into your admin class accessMapping property.',
2203
                $action
2204
            ));
2205
        }
2206
2207
        if (!\is_array($access[$action])) {
2208
            $access[$action] = [$access[$action]];
2209
        }
2210
2211
        foreach ($access[$action] as $role) {
2212
            if (false === $this->isGranted($role, $object)) {
2213
                throw new AccessDeniedException(sprintf('Access Denied to the action %s and role %s', $action, $role));
2214
            }
2215
        }
2216
    }
2217
2218
    /**
2219
     * {@inheritdoc}
2220
     */
2221
    public function hasAccess(string $action, ?object $object = null): bool
2222
    {
2223
        $access = $this->getAccess();
2224
2225
        if (!\array_key_exists($action, $access)) {
2226
            return false;
2227
        }
2228
2229
        if (!\is_array($access[$action])) {
2230
            $access[$action] = [$access[$action]];
2231
        }
2232
2233
        foreach ($access[$action] as $role) {
2234
            if (false === $this->isGranted($role, $object)) {
2235
                return false;
2236
            }
2237
        }
2238
2239
        return true;
2240
    }
2241
2242
    final public function getActionButtons(string $action, ?object $object = null): array
2243
    {
2244
        $buttonList = [];
2245
2246
        if (\in_array($action, ['tree', 'show', 'edit', 'delete', 'list', 'batch'], true)
2247
            && $this->hasAccess('create')
2248
            && $this->hasRoute('create')
2249
        ) {
2250
            $buttonList['create'] = [
2251
                'template' => $this->getTemplateRegistry()->getTemplate('button_create'),
2252
            ];
2253
        }
2254
2255
        if (\in_array($action, ['show', 'delete', 'acl', 'history'], true)
2256
            && $this->canAccessObject('edit', $object)
2257
            && $this->hasRoute('edit')
2258
        ) {
2259
            $buttonList['edit'] = [
2260
                'template' => $this->getTemplateRegistry()->getTemplate('button_edit'),
2261
            ];
2262
        }
2263
2264
        if (\in_array($action, ['show', 'edit', 'acl'], true)
2265
            && $this->canAccessObject('history', $object)
2266
            && $this->hasRoute('history')
2267
        ) {
2268
            $buttonList['history'] = [
2269
                'template' => $this->getTemplateRegistry()->getTemplate('button_history'),
2270
            ];
2271
        }
2272
2273
        if (\in_array($action, ['edit', 'history'], true)
2274
            && $this->isAclEnabled()
2275
            && $this->canAccessObject('acl', $object)
2276
            && $this->hasRoute('acl')
2277
        ) {
2278
            $buttonList['acl'] = [
2279
                'template' => $this->getTemplateRegistry()->getTemplate('button_acl'),
2280
            ];
2281
        }
2282
2283
        if (\in_array($action, ['edit', 'history', 'acl'], true)
2284
            && $this->canAccessObject('show', $object)
2285
            && \count($this->getShow()) > 0
2286
            && $this->hasRoute('show')
2287
        ) {
2288
            $buttonList['show'] = [
2289
                'template' => $this->getTemplateRegistry()->getTemplate('button_show'),
2290
            ];
2291
        }
2292
2293
        if (\in_array($action, ['show', 'edit', 'delete', 'acl', 'batch'], true)
2294
            && $this->hasAccess('list')
2295
            && $this->hasRoute('list')
2296
        ) {
2297
            $buttonList['list'] = [
2298
                'template' => $this->getTemplateRegistry()->getTemplate('button_list'),
2299
            ];
2300
        }
2301
2302
        $buttonList = $this->configureActionButtons($buttonList, $action, $object);
2303
2304
        foreach ($this->getExtensions() as $extension) {
2305
            $buttonList = $extension->configureActionButtons($this, $buttonList, $action, $object);
2306
        }
2307
2308
        return $buttonList;
2309
    }
2310
2311
    /**
2312
     * {@inheritdoc}
2313
     */
2314
    public function getDashboardActions(): array
2315
    {
2316
        $actions = [];
2317
2318
        if ($this->hasRoute('create') && $this->hasAccess('create')) {
2319
            $actions['create'] = [
2320
                'label' => 'link_add',
2321
                'translation_domain' => 'SonataAdminBundle',
2322
                'template' => $this->getTemplateRegistry()->getTemplate('action_create'),
2323
                'url' => $this->generateUrl('create'),
2324
                'icon' => 'plus-circle',
2325
            ];
2326
        }
2327
2328
        if ($this->hasRoute('list') && $this->hasAccess('list')) {
2329
            $actions['list'] = [
2330
                'label' => 'link_list',
2331
                'translation_domain' => 'SonataAdminBundle',
2332
                'url' => $this->generateUrl('list'),
2333
                'icon' => 'list',
2334
            ];
2335
        }
2336
2337
        return $actions;
2338
    }
2339
2340
    /**
2341
     * {@inheritdoc}
2342
     */
2343
    final public function showMosaicButton($isShown): void
2344
    {
2345
        if ($isShown) {
2346
            $this->listModes['mosaic'] = ['class' => static::MOSAIC_ICON_CLASS];
2347
        } else {
2348
            unset($this->listModes['mosaic']);
2349
        }
2350
    }
2351
2352
    final public function getSearchResultLink(object $object): ?string
2353
    {
2354
        foreach ($this->searchResultActions as $action) {
2355
            if ($this->hasRoute($action) && $this->hasAccess($action, $object)) {
2356
                return $this->generateObjectUrl($action, $object);
2357
            }
2358
        }
2359
2360
        return null;
2361
    }
2362
2363
    public function canAccessObject(string $action, ?object $object = null): bool
2364
    {
2365
        if (!\is_object($object)) {
2366
            return false;
2367
        }
2368
        if (!$this->id($object)) {
2369
            return false;
2370
        }
2371
2372
        return $this->hasAccess($action, $object);
2373
    }
2374
2375
    public function configureActionButtons(array $buttonList, string $action, ?object $object = null): array
2376
    {
2377
        return $buttonList;
2378
    }
2379
2380
    /**
2381
     * Hook to run after initialization.
2382
     */
2383
    protected function configure(): void
2384
    {
2385
    }
2386
2387
    protected function configureQuery(ProxyQueryInterface $query): ProxyQueryInterface
2388
    {
2389
        return $query;
2390
    }
2391
2392
    /**
2393
     * urlize the given word.
2394
     *
2395
     * @param string $sep the separator
2396
     */
2397
    final protected function urlize(string $word, string $sep = '_'): string
2398
    {
2399
        return strtolower(preg_replace('/[^a-z0-9_]/i', $sep.'$1', $word));
2400
    }
2401
2402
    final protected function getTemplateRegistry(): MutableTemplateRegistryInterface
2403
    {
2404
        return $this->templateRegistry;
2405
    }
2406
2407
    /**
2408
     * Returns a list of default sort values.
2409
     *
2410
     * @return array{_page?: int, _per_page?: int, _sort_by?: string, _sort_order?: string}
0 ignored issues
show
Documentation introduced by
The doc-type array{_page?: could not be parsed: Unknown type name "array{_page?:" at position 0. (view supported doc-types)

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

Loading history...
2411
     */
2412
    final protected function getDefaultSortValues(): array
2413
    {
2414
        $defaultSortValues = [];
2415
2416
        $this->configureDefaultSortValues($defaultSortValues);
2417
2418
        foreach ($this->getExtensions() as $extension) {
2419
            $extension->configureDefaultSortValues($this, $defaultSortValues);
2420
        }
2421
2422
        return $defaultSortValues;
2423
    }
2424
2425
    /**
2426
     * Returns a list of default filters.
2427
     */
2428
    final protected function getDefaultFilterValues(): array
2429
    {
2430
        $defaultFilterValues = [];
2431
2432
        $this->configureDefaultFilterValues($defaultFilterValues);
2433
2434
        foreach ($this->getExtensions() as $extension) {
2435
            $extension->configureDefaultFilterValues($this, $defaultFilterValues);
2436
        }
2437
2438
        return $defaultFilterValues;
2439
    }
2440
2441
    protected function configureFormFields(FormMapper $form): void
2442
    {
2443
    }
2444
2445
    protected function configureListFields(ListMapper $list): void
2446
    {
2447
    }
2448
2449
    protected function configureDatagridFilters(DatagridMapper $filter): void
2450
    {
2451
    }
2452
2453
    protected function configureShowFields(ShowMapper $show): void
2454
    {
2455
    }
2456
2457
    protected function configureRoutes(RouteCollectionInterface $collection): void
2458
    {
2459
    }
2460
2461
    /**
2462
     * Allows you to customize batch actions.
2463
     *
2464
     * @param array $actions List of actions
2465
     */
2466
    protected function configureBatchActions(array $actions): array
2467
    {
2468
        return $actions;
2469
    }
2470
2471
    /**
2472
     * Configures the tab menu in your admin.
2473
     */
2474
    protected function configureTabMenu(ItemInterface $menu, string $action, ?AdminInterface $childAdmin = null): void
0 ignored issues
show
Unused Code introduced by
The parameter $menu is not used and could be removed.

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

Loading history...
Unused Code introduced by
The parameter $action is not used and could be removed.

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

Loading history...
Unused Code introduced by
The parameter $childAdmin is not used and could be removed.

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

Loading history...
2475
    {
2476
    }
2477
2478
    /**
2479
     * build the view FieldDescription array.
2480
     */
2481
    protected function buildShow(): void
2482
    {
2483
        if ($this->loaded['show']) {
2484
            return;
2485
        }
2486
2487
        $this->loaded['show'] = true;
2488
2489
        $this->show = $this->getShowBuilder()->getBaseList();
2490
        $mapper = new ShowMapper($this->getShowBuilder(), $this->show, $this);
2491
2492
        $this->configureShowFields($mapper);
2493
2494
        foreach ($this->getExtensions() as $extension) {
2495
            $extension->configureShowFields($mapper);
2496
        }
2497
    }
2498
2499
    /**
2500
     * build the list FieldDescription array.
2501
     */
2502
    protected function buildList(): void
2503
    {
2504
        if ($this->loaded['list']) {
2505
            return;
2506
        }
2507
2508
        $this->loaded['list'] = true;
2509
2510
        $this->list = $this->getListBuilder()->getBaseList();
2511
        $mapper = new ListMapper($this->getListBuilder(), $this->list, $this);
2512
2513
        if (\count($this->getBatchActions()) > 0 && $this->hasRequest() && !$this->getRequest()->isXmlHttpRequest()) {
2514
            $fieldDescription = $this->getModelManager()->getNewFieldDescriptionInstance(
2515
                $this->getClass(),
2516
                'batch',
2517
                [
2518
                    'label' => 'batch',
2519
                    'code' => '_batch',
2520
                    'sortable' => false,
2521
                    'virtual_field' => true,
2522
                ]
2523
            );
2524
2525
            $fieldDescription->setAdmin($this);
2526
            $fieldDescription->setTemplate($this->getTemplateRegistry()->getTemplate('batch'));
2527
2528
            $mapper->add($fieldDescription, ListMapper::TYPE_BATCH);
2529
        }
2530
2531
        $this->configureListFields($mapper);
2532
2533
        foreach ($this->getExtensions() as $extension) {
2534
            $extension->configureListFields($mapper);
2535
        }
2536
2537
        if ($this->hasRequest() && $this->getRequest()->isXmlHttpRequest()) {
2538
            $fieldDescription = $this->getModelManager()->getNewFieldDescriptionInstance(
2539
                $this->getClass(),
2540
                'select',
2541
                [
2542
                    'label' => false,
2543
                    'code' => '_select',
2544
                    'sortable' => false,
2545
                    'virtual_field' => false,
2546
                ]
2547
            );
2548
2549
            $fieldDescription->setAdmin($this);
2550
            $fieldDescription->setTemplate($this->getTemplateRegistry()->getTemplate('select'));
2551
2552
            $mapper->add($fieldDescription, ListMapper::TYPE_SELECT);
2553
        }
2554
    }
2555
2556
    /**
2557
     * Build the form FieldDescription collection.
2558
     */
2559
    protected function buildForm(): void
2560
    {
2561
        if ($this->loaded['form']) {
2562
            return;
2563
        }
2564
2565
        $this->loaded['form'] = true;
2566
2567
        $formBuilder = $this->getFormBuilder();
2568
        $formBuilder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event): void {
2569
            $this->preValidate($event->getData());
2570
        }, 100);
2571
2572
        $this->form = $formBuilder->getForm();
2573
    }
2574
2575
    /**
2576
     * Gets the subclass corresponding to the given name.
2577
     *
2578
     * @param string $name The name of the sub class
2579
     *
2580
     * @return string the subclass
2581
     */
2582
    protected function getSubClass(string $name): string
2583
    {
2584
        if ($this->hasSubClass($name)) {
2585
            return $this->subClasses[$name];
2586
        }
2587
2588
        throw new \LogicException(sprintf('Unable to find the subclass `%s` for admin `%s`', $name, static::class));
2589
    }
2590
2591
    /**
2592
     * Attach the inline validator to the model metadata, this must be done once per admin.
2593
     */
2594
    protected function attachInlineValidator(): void
2595
    {
2596
        $admin = $this;
2597
2598
        // add the custom inline validation option
2599
        $metadata = $this->validator->getMetadataFor($this->getClass());
2600
        if (!$metadata instanceof GenericMetadata) {
2601
            throw new \UnexpectedValueException(
2602
                sprintf(
2603
                    'Cannot add inline validator for %s because its metadata is an instance of %s instead of %s',
2604
                    $this->getClass(),
2605
                    \get_class($metadata),
2606
                    GenericMetadata::class
2607
                )
2608
            );
2609
        }
2610
2611
        $metadata->addConstraint(new InlineConstraint([
2612
            'service' => $this,
2613
            'method' => static function (ErrorElement $errorElement, $object) use ($admin): void {
2614
                /* @var \Sonata\AdminBundle\Admin\AdminInterface $admin */
2615
2616
                // This avoid the main validation to be cascaded to children
2617
                // The problem occurs when a model Page has a collection of Page as property
2618
                if ($admin->hasSubject() && spl_object_hash($object) !== spl_object_hash($admin->getSubject())) {
2619
                    return;
2620
                }
2621
2622
                $admin->validate($errorElement, $object);
2623
2624
                foreach ($admin->getExtensions() as $extension) {
2625
                    $extension->validate($admin, $errorElement, $object);
2626
                }
2627
            },
2628
            'serializingWarning' => true,
2629
        ]));
2630
    }
2631
2632
    /**
2633
     * Return list routes with permissions name.
2634
     *
2635
     * @return array<string, string|array>
0 ignored issues
show
Documentation introduced by
The doc-type array<string, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

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

Loading history...
2636
     */
2637
    protected function getAccess(): array
2638
    {
2639
        $access = array_merge([
2640
            'acl' => 'MASTER',
2641
            'export' => 'EXPORT',
2642
            'historyCompareRevisions' => 'EDIT',
2643
            'historyViewRevision' => 'EDIT',
2644
            'history' => 'EDIT',
2645
            'edit' => 'EDIT',
2646
            'show' => 'VIEW',
2647
            'create' => 'CREATE',
2648
            'delete' => 'DELETE',
2649
            'batchDelete' => 'DELETE',
2650
            'list' => 'LIST',
2651
        ], $this->getAccessMapping());
2652
2653
        foreach ($this->extensions as $extension) {
2654
            $access = array_merge($access, $extension->getAccessMapping($this));
2655
        }
2656
2657
        return $access;
2658
    }
2659
2660
    /**
2661
     * Configures a list of default filters.
2662
     */
2663
    protected function configureDefaultFilterValues(array &$filterValues): void
2664
    {
2665
    }
2666
2667
    /**
2668
     * Configures a list of default sort values.
2669
     *
2670
     * Example:
2671
     *   $sortValues['_sort_by'] = 'foo'
2672
     *   $sortValues['_sort_order'] = 'DESC'
2673
     */
2674
    protected function configureDefaultSortValues(array &$sortValues)
0 ignored issues
show
Unused Code introduced by
The parameter $sortValues is not used and could be removed.

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

Loading history...
2675
    {
2676
    }
2677
2678
    /**
2679
     * Set the parent object, if any, to the provided object.
2680
     */
2681
    final protected function appendParentObject(object $object): void
2682
    {
2683
        if ($this->isChild() && $this->getParentAssociationMapping()) {
2684
            $parentAdmin = $this->getParent();
2685
            $parentObject = $parentAdmin->getObject($this->request->get($parentAdmin->getIdParameter()));
2686
2687
            if (null !== $parentObject) {
2688
                $propertyAccessor = $this->getConfigurationPool()->getPropertyAccessor();
2689
                $propertyPath = new PropertyPath($this->getParentAssociationMapping());
2690
2691
                $value = $propertyAccessor->getValue($object, $propertyPath);
2692
2693
                if (\is_array($value) || $value instanceof \ArrayAccess) {
2694
                    $value[] = $parentObject;
2695
                    $propertyAccessor->setValue($object, $propertyPath, $value);
2696
                } else {
2697
                    $propertyAccessor->setValue($object, $propertyPath, $parentObject);
2698
                }
2699
            }
2700
        } elseif ($this->hasParentFieldDescription()) {
2701
            $parentAdmin = $this->getParentFieldDescription()->getAdmin();
2702
            $parentObject = $parentAdmin->getObject($this->request->get($parentAdmin->getIdParameter()));
2703
2704
            if (null !== $parentObject) {
2705
                ObjectManipulator::setObject($object, $parentObject, $this->getParentFieldDescription());
2706
            }
2707
        }
2708
    }
2709
2710
    /**
2711
     * {@inheritdoc}
2712
     */
2713
    private function buildDatagrid(): void
2714
    {
2715
        if ($this->loaded['datagrid']) {
2716
            return;
2717
        }
2718
2719
        $this->loaded['datagrid'] = true;
2720
2721
        $filterParameters = $this->getFilterParameters();
2722
2723
        // transform _sort_by from a string to a FieldDescriptionInterface for the datagrid.
2724
        if (isset($filterParameters['_sort_by']) && \is_string($filterParameters['_sort_by'])) {
2725
            if ($this->hasListFieldDescription($filterParameters['_sort_by'])) {
2726
                $filterParameters['_sort_by'] = $this->getListFieldDescription($filterParameters['_sort_by']);
2727
            } else {
2728
                $filterParameters['_sort_by'] = $this->getModelManager()->getNewFieldDescriptionInstance(
2729
                    $this->getClass(),
2730
                    $filterParameters['_sort_by'],
2731
                    []
2732
                );
2733
2734
                $this->getListBuilder()->buildField(null, $filterParameters['_sort_by'], $this);
2735
            }
2736
        }
2737
2738
        // initialize the datagrid
2739
        $this->datagrid = $this->getDatagridBuilder()->getBaseDatagrid($this, $filterParameters);
2740
2741
        $this->datagrid->getPager()->setMaxPageLinks($this->maxPageLinks);
2742
2743
        $mapper = new DatagridMapper($this->getDatagridBuilder(), $this->datagrid, $this);
2744
2745
        // build the datagrid filter
2746
        $this->configureDatagridFilters($mapper);
2747
2748
        // ok, try to limit to add parent filter
2749
        if ($this->isChild() && $this->getParentAssociationMapping() && !$mapper->has($this->getParentAssociationMapping())) {
2750
            $mapper->add($this->getParentAssociationMapping(), null, [
2751
                'show_filter' => false,
2752
                'label' => false,
2753
                'field_type' => ModelHiddenType::class,
2754
                'field_options' => [
2755
                    'model_manager' => $this->getModelManager(),
2756
                ],
2757
                'operator_type' => HiddenType::class,
2758
            ], null, null, [
2759
                'admin_code' => $this->getParent()->getCode(),
2760
            ]);
2761
        }
2762
2763
        foreach ($this->getExtensions() as $extension) {
2764
            $extension->configureDatagridFilters($mapper);
2765
        }
2766
    }
2767
2768
    /**
2769
     * Build all the related urls to the current admin.
2770
     */
2771
    private function buildRoutes(): void
2772
    {
2773
        if ($this->loaded['routes']) {
2774
            return;
2775
        }
2776
2777
        $this->loaded['routes'] = true;
2778
2779
        $this->routes = new RouteCollection(
2780
            $this->getBaseCodeRoute(),
2781
            $this->getBaseRouteName(),
2782
            $this->getBaseRoutePattern(),
2783
            $this->getBaseControllerName()
2784
        );
2785
2786
        $this->routeBuilder->build($this, $this->routes);
2787
2788
        $this->configureRoutes($this->routes);
2789
2790
        foreach ($this->getExtensions() as $extension) {
2791
            $extension->configureRoutes($this, $this->routes);
2792
        }
2793
    }
2794
}
2795