Passed
Push — master ( 97e8d4...784af9 )
by Raissa
05:32 queued 03:00
created

BaseElement   F

Complexity

Total Complexity 110

Size/Duplication

Total Lines 955
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 306
dl 0
loc 955
rs 2
c 0
b 0
f 0
wmc 110

45 Methods

Rating   Name   Duplication   Size   Complexity  
A getType() 0 3 1
A getController() 0 18 3
A setController() 0 5 1
A inlineEditable() 0 3 1
A provideBlockSchema() 0 7 1
A Pos() 0 3 1
A getAuthor() 0 7 2
A First() 0 3 1
A sanitiseClassName() 0 3 1
A getBlockSchema() 0 7 1
A TotalItems() 0 3 1
A getPageTitle() 0 9 2
A getPage() 0 15 4
A getEditorPreview() 0 6 1
A Top() 0 3 2
A unsanitiseClassName() 0 3 1
A getTypeNice() 0 8 2
A canDelete() 0 14 5
A canView() 0 14 5
A getSummary() 0 3 1
A AbsoluteLink() 0 9 2
A getDescription() 0 7 2
A stripNamespacing() 0 4 1
B getAreaRelationName() 0 29 8
A PageCMSEditLink() 0 11 2
A getMimeType() 0 3 1
A CMSEditLink() 0 39 5
A getRenderTemplates() 0 32 6
A canEdit() 0 14 5
A Last() 0 3 1
A getStyleVariant() 0 14 2
A Link() 0 11 2
A PreviewLink() 0 7 1
B getAnchor() 0 33 8
A EvenOdd() 0 5 2
A getSimpleClassName() 0 3 1
A setAreaRelationNameCache() 0 5 1
A updateFromFormData() 0 13 3
A getCMSFields() 0 58 3
A canCreate() 0 8 3
A isCMSPreview() 0 11 3
A getEditLink() 0 3 1
A forTemplate() 0 9 2
A onBeforeWrite() 0 14 3
A getIcon() 0 30 5

How to fix   Complexity   

Complex Class

Complex classes like BaseElement often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use BaseElement, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace DNADesign\Elemental\Models;
4
5
use DNADesign\Elemental\Controllers\ElementController;
6
use DNADesign\Elemental\Forms\EditFormFactory;
7
use DNADesign\Elemental\Forms\TextCheckboxGroupField;
8
use DNADesign\Elemental\ORM\FieldType\DBObjectType;
9
use Exception;
10
use SilverStripe\CMS\Controllers\CMSPageEditController;
11
use SilverStripe\CMS\Model\SiteTree;
12
use SilverStripe\Control\Controller;
13
use SilverStripe\Control\Director;
14
use SilverStripe\Core\ClassInfo;
15
use SilverStripe\Core\Injector\Injector;
16
use SilverStripe\Forms\CheckboxField;
17
use SilverStripe\Forms\DropdownField;
18
use SilverStripe\Forms\FieldList;
19
use SilverStripe\Forms\HiddenField;
20
use SilverStripe\Forms\NumericField;
21
use SilverStripe\Forms\TextField;
22
use SilverStripe\ORM\DataObject;
23
use SilverStripe\ORM\FieldType\DBBoolean;
24
use SilverStripe\ORM\FieldType\DBField;
25
use SilverStripe\ORM\FieldType\DBHTMLText;
26
use SilverStripe\Security\Member;
27
use SilverStripe\Security\Permission;
28
use SilverStripe\Versioned\Versioned;
29
use SilverStripe\View\ArrayData;
30
use SilverStripe\View\Parsers\URLSegmentFilter;
31
32
/**
33
 * Class BaseElement
34
 * @package DNADesign\Elemental\Models
35
 *
36
 * @property string $Title
37
 * @property bool $ShowTitle
38
 * @property int $Sort
39
 * @property string $ExtraClass
40
 * @property string $Style
41
 *
42
 * @method ElementalArea Parent()
43
 */
44
class BaseElement extends DataObject
45
{
46
    /**
47
     * Override this on your custom elements to specify a CSS icon class
48
     *
49
     * @var string
50
     */
51
    private static $icon = 'font-icon-block-layout';
0 ignored issues
show
introduced by Robbie Averill
The private property $icon is not used, and could be removed.
Loading history...
52
53
    /**
54
     * Describe the purpose of this element
55
     *
56
     * @config
57
     * @var string
58
     */
59
    private static $description = 'Base element class';
0 ignored issues
show
introduced by Robbie Averill
The private property $description is not used, and could be removed.
Loading history...
60
61
    private static $db = [
0 ignored issues
show
introduced by Will Rossiter
The private property $db is not used, and could be removed.
Loading history...
62
        'Title' => 'Varchar(255)',
63
        'ShowTitle' => 'Boolean',
64
        'Sort' => 'Int',
65
        'ExtraClass' => 'Varchar(255)',
66
        'Style' => 'Varchar(255)'
67
    ];
68
69
    private static $has_one = [
0 ignored issues
show
introduced by Will Rossiter
The private property $has_one is not used, and could be removed.
Loading history...
70
        'Parent' => ElementalArea::class
71
    ];
72
73
    private static $extensions = [
0 ignored issues
show
introduced by Will Rossiter
The private property $extensions is not used, and could be removed.
Loading history...
74
        Versioned::class
75
    ];
76
77
    private static $casting = [
0 ignored issues
show
introduced by Guy Marriott
The private property $casting is not used, and could be removed.
Loading history...
78
        'BlockSchema' => DBObjectType::class,
79
        'InlineEditable' => DBBoolean::class,
80
        'IsLiveVersion' => DBBoolean::class,
81
        'IsPublished' => DBBoolean::class,
82
    ];
83
84
    private static $versioned_gridfield_extensions = true;
0 ignored issues
show
introduced by Will Rossiter
The private property $versioned_gridfield_extensions is not used, and could be removed.
Loading history...
85
86
    private static $table_name = 'Element';
0 ignored issues
show
introduced by John Milmine
The private property $table_name is not used, and could be removed.
Loading history...
87
88
    /**
89
     * @var string
90
     */
91
    private static $controller_class = ElementController::class;
92
93
    /**
94
     * @var string
95
     */
96
    private static $controller_template = 'ElementHolder';
0 ignored issues
show
introduced by Will Rossiter
The private property $controller_template is not used, and could be removed.
Loading history...
97
98
    /**
99
     * @var ElementController
100
     */
101
    protected $controller;
102
103
    /**
104
     * Cache various data to improve CMS load time
105
     *
106
     * @internal
107
     * @var array
108
     */
109
    protected $cacheData;
110
111
    private static $default_sort = 'Sort';
0 ignored issues
show
introduced by John Milmine
The private property $default_sort is not used, and could be removed.
Loading history...
112
113
    private static $singular_name = 'block';
0 ignored issues
show
introduced by Robbie Averill
The private property $singular_name is not used, and could be removed.
Loading history...
114
115
    private static $plural_name = 'blocks';
0 ignored issues
show
introduced by Robbie Averill
The private property $plural_name is not used, and could be removed.
Loading history...
116
117
    private static $summary_fields = [
0 ignored issues
show
introduced by Will Rossiter
The private property $summary_fields is not used, and could be removed.
Loading history...
118
        'EditorPreview' => 'Summary'
119
    ];
120
121
    /**
122
     * @config
123
     * @var array
124
     */
125
    private static $styles = [];
0 ignored issues
show
introduced by Will Rossiter
The private property $styles is not used, and could be removed.
Loading history...
126
127
    private static $searchable_fields = [
0 ignored issues
show
introduced by Will Rossiter
The private property $searchable_fields is not used, and could be removed.
Loading history...
128
        'ID' => [
129
            'field' => NumericField::class,
130
        ],
131
        'Title',
132
        'LastEdited'
133
    ];
134
135
    /**
136
     * Enable for backwards compatibility
137
     *
138
     * @var boolean
139
     */
140
    private static $disable_pretty_anchor_name = false;
141
142
    /**
143
     * Set to false to prevent an in-line edit form from showing in an elemental area. Instead the element will be
144
     * clickable and a GridFieldDetailForm will be used.
145
     *
146
     * @config
147
     * @var bool
148
     */
149
    private static $inline_editable = true;
0 ignored issues
show
introduced by Guy Marriott
The private property $inline_editable is not used, and could be removed.
Loading history...
150
151
    /**
152
     * Store used anchor names, this is to avoid title clashes
153
     * when calling 'getAnchor'
154
     *
155
     * @var array
156
     */
157
    protected static $used_anchors = [];
158
159
    /**
160
     * For caching 'getAnchor'
161
     *
162
     * @var string
163
     */
164
    protected $anchor = null;
165
166
    /**
167
     * Basic permissions, defaults to page perms where possible.
168
     *
169
     * @param Member $member
170
     * @return boolean
171
     */
172
    public function canView($member = null)
173
    {
174
        $extended = $this->extendedCan(__FUNCTION__, $member);
175
        if ($extended !== null) {
176
            return $extended;
177
        }
178
179
        if ($this->hasMethod('getPage')) {
180
            if ($page = $this->getPage()) {
181
                return $page->canView($member);
182
            }
183
        }
184
185
        return (Permission::check('CMS_ACCESS', 'any', $member)) ? true : null;
186
    }
187
188
    /**
189
     * Basic permissions, defaults to page perms where possible.
190
     *
191
     * @param Member $member
192
     *
193
     * @return boolean
194
     */
195
    public function canEdit($member = null)
196
    {
197
        $extended = $this->extendedCan(__FUNCTION__, $member);
198
        if ($extended !== null) {
199
            return $extended;
200
        }
201
202
        if ($this->hasMethod('getPage')) {
203
            if ($page = $this->getPage()) {
204
                return $page->canEdit($member);
205
            }
206
        }
207
208
        return (Permission::check('CMS_ACCESS', 'any', $member)) ? true : null;
209
    }
210
211
    /**
212
     * Basic permissions, defaults to page perms where possible.
213
     *
214
     * Uses archive not delete so that current stage is respected i.e if a
215
     * element is not published, then it can be deleted by someone who doesn't
216
     * have publishing permissions.
217
     *
218
     * @param Member $member
219
     *
220
     * @return boolean
221
     */
222
    public function canDelete($member = null)
223
    {
224
        $extended = $this->extendedCan(__FUNCTION__, $member);
225
        if ($extended !== null) {
226
            return $extended;
227
        }
228
229
        if ($this->hasMethod('getPage')) {
230
            if ($page = $this->getPage()) {
231
                return $page->canArchive($member);
232
            }
233
        }
234
235
        return (Permission::check('CMS_ACCESS', 'any', $member)) ? true : null;
236
    }
237
238
    /**
239
     * Basic permissions, defaults to page perms where possible.
240
     *
241
     * @param Member $member
242
     * @param array $context
243
     *
244
     * @return boolean
245
     */
246
    public function canCreate($member = null, $context = array())
247
    {
248
        $extended = $this->extendedCan(__FUNCTION__, $member);
249
        if ($extended !== null) {
250
            return $extended;
251
        }
252
253
        return (Permission::check('CMS_ACCESS', 'any', $member)) ? true : null;
254
    }
255
256
    /**
257
     * Increment the sort order if one hasn't been already defined. This
258
     * ensures that new elements are created at the end of the list by default.
259
     *
260
     * {@inheritDoc}
261
     */
262
    public function onBeforeWrite()
263
    {
264
        parent::onBeforeWrite();
265
266
        if (!$this->Sort) {
267
            if ($this->hasExtension(Versioned::class)) {
268
                $records = Versioned::get_by_stage(BaseElement::class, Versioned::DRAFT);
269
            } else {
270
                $records = BaseElement::get();
271
            }
272
273
            $records = $records->filter('ParentID', $this->ParentID);
0 ignored issues
show
Bug Best Practice introduced by Will Rossiter
The property ParentID does not exist on DNADesign\Elemental\Models\BaseElement. Since you implemented __get, consider adding a @property annotation.
Loading history...
274
275
            $this->Sort = $records->max('Sort') + 1;
276
        }
277
    }
278
279
    public function getCMSFields()
280
    {
281
        $this->beforeUpdateCMSFields(function (FieldList $fields) {
282
            // Remove relationship fields
283
            $fields->removeByName('ParentID');
284
            $fields->removeByName('Sort');
285
286
            // Remove link and file tracking tabs
287
            $fields->removeByName(['LinkTracking', 'FileTracking']);
288
289
            $fields->addFieldToTab(
290
                'Root.Settings',
291
                TextField::create('ExtraClass', _t(__CLASS__ . '.ExtraCssClassesLabel', 'Custom CSS classes'))
292
                    ->setAttribute(
293
                        'placeholder',
294
                        _t(__CLASS__ . '.ExtraCssClassesPlaceholder', 'my_class another_class')
295
                    )
296
            );
297
298
            // Add a combined field for "Title" and "Displayed" checkbox in a Bootstrap input group
299
            $fields->removeByName('ShowTitle');
300
            $fields->replaceField(
301
                'Title',
302
                TextCheckboxGroupField::create(
303
                    TextField::create('Title', _t(__CLASS__ . '.TitleLabel', 'Title (displayed if checked)')),
304
                    CheckboxField::create('ShowTitle', _t(__CLASS__ . '.ShowTitleLabel', 'Displayed'))
305
                )
306
                    ->setName('TitleAndDisplayed')
307
            );
308
309
            // Rename the "Main" tab
310
            $fields->fieldByName('Root.Main')
311
                ->setTitle(_t(__CLASS__ . '.MainTabLabel', 'Content'));
312
313
            $fields->addFieldsToTab('Root.Main', [
314
                HiddenField::create('AbsoluteLink', false, Director::absoluteURL($this->PreviewLink())),
315
                HiddenField::create('LiveLink', false, Director::absoluteURL($this->Link())),
316
                HiddenField::create('StageLink', false, Director::absoluteURL($this->PreviewLink())),
317
            ]);
318
319
            $styles = $this->config()->get('styles');
320
321
            if ($styles && count($styles) > 0) {
322
                $styleDropdown = DropdownField::create('Style', _t(__CLASS__.'.STYLE', 'Style variation'), $styles);
323
324
                $fields->insertBefore($styleDropdown, 'ExtraClass');
0 ignored issues
show
Bug introduced by Will Rossiter
It seems like $styleDropdown can also be of type object; however, parameter $name of SilverStripe\Forms\FieldList::insertBefore() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

324
                $fields->insertBefore(/** @scrutinizer ignore-type */ $styleDropdown, 'ExtraClass');
Loading history...
Bug introduced by Will Rossiter
'ExtraClass' of type string is incompatible with the type SilverStripe\Forms\FormField expected by parameter $item of SilverStripe\Forms\FieldList::insertBefore(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

324
                $fields->insertBefore($styleDropdown, /** @scrutinizer ignore-type */ 'ExtraClass');
Loading history...
325
326
                $styleDropdown->setEmptyString(_t(__CLASS__.'.CUSTOM_STYLES', 'Select a style..'));
327
            } else {
328
                $fields->removeByName('Style');
329
            }
330
331
            // Hide the navigation section of the tabs in the React component {@see silverstripe/admin Tabs}
332
            $rootTabset = $fields->fieldByName('Root');
333
            $rootTabset->setSchemaState(['hideNav' => true]);
334
        });
335
336
        return parent::getCMSFields();
337
    }
338
339
    /**
340
     * Get the type of the current block, for use in GridField summaries, block
341
     * type dropdowns etc. Examples are "Content", "File", "Media", etc.
342
     *
343
     * @return string
344
     */
345
    public function getType()
346
    {
347
        return _t(__CLASS__ . '.BlockType', 'Block');
348
    }
349
350
    /**
351
     * Proxy through to configuration setting 'inline_editable'
352
     *
353
     * @return bool
354
     */
355
    public function inlineEditable()
356
    {
357
        return static::config()->get('inline_editable');
358
    }
359
360
    /**
361
     * @param ElementController $controller
362
     *
363
     * @return $this
364
     */
365
    public function setController($controller)
366
    {
367
        $this->controller = $controller;
368
369
        return $this;
370
    }
371
372
    /**
373
     * @throws Exception If the specified controller class doesn't exist
374
     *
375
     * @return ElementController
376
     */
377
    public function getController()
378
    {
379
        if ($this->controller) {
380
            return $this->controller;
381
        }
382
383
        $controllerClass = self::config()->controller_class;
384
385
        if (!class_exists($controllerClass)) {
386
            throw new Exception(
387
                'Could not find controller class ' . $controllerClass . ' as defined in ' . static::class
388
            );
389
        }
390
391
        $this->controller = Injector::inst()->create($controllerClass, $this);
392
        $this->controller->doInit();
393
394
        return $this->controller;
395
    }
396
397
    /**
398
     * @param string $name
399
     * @return $this
400
     */
401
    public function setAreaRelationNameCache($name)
402
    {
403
        $this->cacheData['area_relation_name'] = $name;
404
405
        return $this;
406
    }
407
408
    /**
409
     * @return Controller
410
     */
411
    public function Top()
412
    {
413
        return (Controller::has_curr()) ? Controller::curr() : null;
414
    }
415
416
    /**
417
     * Default way to render element in templates. Note that all blocks should
418
     * be rendered through their {@link ElementController} class as this
419
     * contains the holder styles.
420
     *
421
     * @return string|null HTML
422
     */
423
    public function forTemplate($holder = true)
424
    {
425
        $templates = $this->getRenderTemplates();
426
427
        if ($templates) {
428
            return $this->renderWith($templates);
429
        }
430
431
        return null;
432
    }
433
434
    /**
435
     * @param string $suffix
436
     *
437
     * @return array
438
     */
439
    public function getRenderTemplates($suffix = '')
440
    {
441
        $classes = ClassInfo::ancestry($this->ClassName);
442
        $classes[static::class] = static::class;
443
        $classes = array_reverse($classes);
444
        $templates = [];
445
446
        foreach ($classes as $key => $class) {
447
            if ($class == BaseElement::class) {
448
                continue;
449
            }
450
451
            if ($class == DataObject::class) {
452
                break;
453
            }
454
455
            if ($style = $this->Style) {
456
                $templates[$class][] = $class . $suffix . '_'. $this->getAreaRelationName() . '_' . $style;
457
                $templates[$class][] = $class . $suffix . '_' . $style;
458
            }
459
            $templates[$class][] = $class . $suffix . '_'. $this->getAreaRelationName();
460
            $templates[$class][] = $class . $suffix;
461
        }
462
463
        $this->extend('updateRenderTemplates', $templates, $suffix);
464
465
        $templateFlat = [];
466
        foreach ($templates as $class => $variations) {
467
            $templateFlat = array_merge($templateFlat, $variations);
468
        }
469
470
        return $templateFlat;
471
    }
472
473
    /**
474
     * Given form data (wit
475
     *
476
     * @param $data
477
     */
478
    public function updateFromFormData($data)
479
    {
480
        $cmsFields = $this->getCMSFields();
481
482
        foreach ($data as $field => $datum) {
483
            $field = $cmsFields->dataFieldByName($field);
484
485
            if (!$field) {
486
                continue;
487
            }
488
489
            $field->setSubmittedValue($datum);
490
            $field->saveInto($this);
491
        }
492
    }
493
494
    /**
495
     * Strip all namespaces from class namespace.
496
     *
497
     * @param string $classname e.g. "\Fully\Namespaced\Class"
498
     *
499
     * @return string following the param example, "Class"
500
     */
501
    protected function stripNamespacing($classname)
502
    {
503
        $classParts = explode('\\', $classname);
504
        return array_pop($classParts);
505
    }
506
507
    /**
508
     * @return string
509
     */
510
    public function getSimpleClassName()
511
    {
512
        return strtolower($this->sanitiseClassName($this->ClassName, '__'));
513
    }
514
515
    /**
516
     * @return null|DataObject
517
     * @throws \Psr\Container\NotFoundExceptionInterface
518
     * @throws \SilverStripe\ORM\ValidationException
519
     */
520
    public function getPage()
521
    {
522
        // Allow for repeated calls to be cached
523
        if (isset($this->cacheData['page'])) {
524
            return $this->cacheData['page'];
525
        }
526
527
        $area = $this->Parent();
528
529
        if ($area instanceof ElementalArea && $area->exists()) {
530
            $this->cacheData['page'] = $area->getOwnerPage();
531
            return $this->cacheData['page'];
532
        }
533
534
        return null;
535
    }
536
537
    /**
538
     * Get a unique anchor name
539
     *
540
     * @return string
541
     */
542
    public function getAnchor()
543
    {
544
        if ($this->anchor !== null) {
545
            return $this->anchor;
546
        }
547
548
        $anchorTitle = '';
549
550
        if (!$this->config()->disable_pretty_anchor_name) {
551
            if ($this->hasMethod('getAnchorTitle')) {
552
                $anchorTitle = $this->getAnchorTitle();
0 ignored issues
show
Bug introduced by John Milmine
The method getAnchorTitle() does not exist on DNADesign\Elemental\Models\BaseElement. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

552
                /** @scrutinizer ignore-call */ 
553
                $anchorTitle = $this->getAnchorTitle();
Loading history...
553
            } elseif ($this->config()->enable_title_in_template) {
554
                $anchorTitle = $this->getField('Title');
555
            }
556
        }
557
558
        if (!$anchorTitle) {
559
            $anchorTitle = 'e'.$this->ID;
560
        }
561
562
        $filter = URLSegmentFilter::create();
563
        $titleAsURL = $filter->filter($anchorTitle);
564
565
        // Ensure that this anchor name isn't already in use
566
        // ie. If two elemental blocks have the same title, it'll append '-2', '-3'
567
        $result = $titleAsURL;
568
        $count = 1;
569
        while (isset(self::$used_anchors[$result]) && self::$used_anchors[$result] !== $this->ID) {
570
            ++$count;
571
            $result = $titleAsURL . '-' . $count;
572
        }
573
        self::$used_anchors[$result] = $this->ID;
574
        return $this->anchor = $result;
575
    }
576
577
    /**
578
     * @param string|null $action
579
     * @return string|null
580
     * @throws \Psr\Container\NotFoundExceptionInterface
581
     * @throws \SilverStripe\ORM\ValidationException
582
     */
583
    public function AbsoluteLink($action = null)
584
    {
585
        if ($page = $this->getPage()) {
586
            $link = $page->AbsoluteLink($action) . '#' . $this->getAnchor();
587
588
            return $link;
589
        }
590
591
        return null;
592
    }
593
594
    /**
595
     * @param string|null $action
596
     * @return string
597
     * @throws \Psr\Container\NotFoundExceptionInterface
598
     * @throws \SilverStripe\ORM\ValidationException
599
     */
600
    public function Link($action = null)
601
    {
602
        if ($page = $this->getPage()) {
603
            $link = $page->Link($action) . '#' . $this->getAnchor();
604
605
            $this->extend('updateLink', $link);
606
607
            return $link;
608
        }
609
610
        return null;
611
    }
612
613
    /**
614
     * @param string|null $action
615
     * @return string
616
     * @throws \Psr\Container\NotFoundExceptionInterface
617
     * @throws \SilverStripe\ORM\ValidationException
618
     */
619
    public function PreviewLink($action = null)
620
    {
621
        $action = $action . '?ElementalPreview=' . mt_rand();
622
        $link = $this->Link($action);
623
        $this->extend('updatePreviewLink', $link);
624
625
        return $link;
626
    }
627
628
    /**
629
     * @return boolean
630
     */
631
    public function isCMSPreview()
632
    {
633
        if (Controller::has_curr()) {
634
            $controller = Controller::curr();
635
636
            if ($controller->getRequest()->requestVar('CMSPreview')) {
637
                return true;
638
            }
639
        }
640
641
        return false;
642
    }
643
644
    /**
645
     * @return null|string
646
     * @throws \Psr\Container\NotFoundExceptionInterface
647
     * @throws \SilverStripe\ORM\ValidationException
648
     */
649
    public function CMSEditLink()
650
    {
651
        // Allow for repeated calls to be returned from cache
652
        if (isset($this->cacheData['cms_edit_link'])) {
653
            return $this->cacheData['cms_edit_link'];
654
        }
655
656
        $relationName = $this->getAreaRelationName();
657
        $page = $this->getPage();
658
659
        if (!$page) {
660
            return null;
661
        }
662
663
        $editLinkPrefix = '';
664
        if (!$page instanceof SiteTree && method_exists($page, 'CMSEditLink')) {
665
            $editLinkPrefix = Controller::join_links($page->CMSEditLink(), 'ItemEditForm');
666
        } else {
667
            $editLinkPrefix = Controller::join_links(
668
                singleton(CMSPageEditController::class)->Link('EditForm'),
669
                $page->ID
670
            );
671
        }
672
673
        $link = Controller::join_links(
674
            $editLinkPrefix,
675
            'field/' . $relationName . '/item/',
676
            $this->ID
677
        );
678
679
        $link = Controller::join_links(
680
            $link,
681
            'edit'
682
        );
683
684
        $this->extend('updateCMSEditLink', $link);
685
686
        $this->cacheData['cms_edit_link'] = $link;
687
        return $link;
688
    }
689
690
    /**
691
     * Retrieve a elemental area relation for creating cms links
692
     *
693
     * @return int|string The name of a valid elemental area relation
694
     * @throws \Psr\Container\NotFoundExceptionInterface
695
     * @throws \SilverStripe\ORM\ValidationException
696
     */
697
    public function getAreaRelationName()
698
    {
699
        // Allow repeated calls to return from internal cache
700
        if (isset($this->cacheData['area_relation_name'])) {
701
            return $this->cacheData['area_relation_name'];
702
        }
703
704
        $page = $this->getPage();
705
706
        $result = 'ElementalArea';
707
708
        if ($page) {
709
            $has_one = $page->config()->get('has_one');
710
            $area = $this->Parent();
711
712
            foreach ($has_one as $relationName => $relationClass) {
713
                if ($page instanceof BaseElement && $relationName === 'Parent') {
714
                    continue;
715
                }
716
                if ($relationClass === $area->ClassName && $page->{$relationName}()->ID === $area->ID) {
717
                    $result = $relationName;
718
                    break;
719
                }
720
            }
721
        }
722
723
        $this->setAreaRelationNameCache($result);
724
725
        return $result;
726
    }
727
728
    /**
729
     * Sanitise a model class' name for inclusion in a link.
730
     *
731
     * @return string
732
     */
733
    public function sanitiseClassName($class, $delimiter = '-')
734
    {
735
        return str_replace('\\', $delimiter, $class);
736
    }
737
738
    public function unsanitiseClassName($class, $delimiter = '-')
739
    {
740
        return str_replace($delimiter, '\\', $class);
741
    }
742
743
    /**
744
     * @return null|string
745
     * @throws \Psr\Container\NotFoundExceptionInterface
746
     * @throws \SilverStripe\ORM\ValidationException
747
     */
748
    public function getEditLink()
749
    {
750
        return $this->CMSEditLink();
751
    }
752
753
    /**
754
     * @return DBField|null
755
     * @throws \Psr\Container\NotFoundExceptionInterface
756
     * @throws \SilverStripe\ORM\ValidationException
757
     */
758
    public function PageCMSEditLink()
759
    {
760
        if ($page = $this->getPage()) {
761
            return DBField::create_field('HTMLText', sprintf(
762
                '<a href="%s">%s</a>',
763
                $page->CMSEditLink(),
764
                $page->Title
765
            ));
766
        }
767
768
        return null;
769
    }
770
771
    /**
772
     * @return string
773
     */
774
    public function getMimeType()
775
    {
776
        return 'text/html';
777
    }
778
779
    /**
780
     * This can be overridden on child elements to create a summary for display
781
     * in GridFields.
782
     *
783
     * @return string
784
     */
785
    public function getSummary()
786
    {
787
        return '';
788
    }
789
790
791
    /**
792
     * The block schema defines a set of data that will be serialised and sent via GraphQL
793
     * to the React editor client.
794
     *
795
     * To modify the schema, either use the extension point or overload the `provideBlockSchema`
796
     * method.
797
     *
798
     * @internal This API may change in future. Treat this as a `final` method.
799
     * @return array
800
     */
801
    public function getBlockSchema()
802
    {
803
        $blockSchema = $this->provideBlockSchema();
804
805
        $this->extend('updateBlockSchema', $blockSchema);
806
807
        return $blockSchema;
808
    }
809
810
    /**
811
     * Provide block schema data, which will be serialised and sent via GraphQL to the editor client.
812
     *
813
     * Overload this method in child element classes to augment, or use the extension point on `getBlockSchema`
814
     * to update it from an `Extension`.
815
     *
816
     * @return array
817
     */
818
    protected function provideBlockSchema()
819
    {
820
        return [
821
            'iconClass' => $this->config()->get('icon'),
822
            'type' => $this->getType(),
823
            'actions' => [
824
                'edit' => $this->getEditLink(),
825
            ],
826
        ];
827
    }
828
829
    /**
830
     * Generate markup for element type icons suitable for use in GridFields.
831
     *
832
     * @return null|DBHTMLText
833
     */
834
    public function getIcon()
835
    {
836
        $data = ArrayData::create([]);
837
838
        $iconClass = $this->config()->get('icon');
839
        if ($iconClass) {
840
            $data->IconClass = $iconClass;
841
842
            // Add versioned states (rendered as a circle over the icon)
843
            if ($this->hasExtension(Versioned::class)) {
844
                $data->IsVersioned = true;
845
                if ($this->isOnDraftOnly()) {
0 ignored issues
show
Bug introduced by Robbie Averill
The method isOnDraftOnly() does not exist on DNADesign\Elemental\Models\BaseElement. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

845
                if ($this->/** @scrutinizer ignore-call */ isOnDraftOnly()) {
Loading history...
846
                    $data->VersionState = 'draft';
847
                    $data->VersionStateTitle = _t(
848
                        'SilverStripe\\Versioned\\VersionedGridFieldState\\VersionedGridFieldState.ADDEDTODRAFTHELP',
849
                        'Item has not been published yet'
850
                    );
851
                } elseif ($this->isModifiedOnDraft()) {
0 ignored issues
show
Bug introduced by Robbie Averill
The method isModifiedOnDraft() does not exist on DNADesign\Elemental\Models\BaseElement. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

851
                } elseif ($this->/** @scrutinizer ignore-call */ isModifiedOnDraft()) {
Loading history...
852
                    $data->VersionState = 'modified';
853
                    $data->VersionStateTitle = $data->VersionStateTitle = _t(
854
                        'SilverStripe\\Versioned\\VersionedGridFieldState\\VersionedGridFieldState.MODIFIEDONDRAFTHELP',
855
                        'Item has unpublished changes'
856
                    );
857
                }
858
            }
859
860
            return $data->renderWith(__CLASS__ . '/PreviewIcon');
861
        }
862
863
        return null;
864
    }
865
866
    /**
867
     * Get a description for this content element, if available
868
     *
869
     * @return string
870
     */
871
    public function getDescription()
872
    {
873
        $description = $this->config()->uninherited('description');
874
        if ($description) {
875
            return _t(__CLASS__ . '.Description', $description);
876
        }
877
        return '';
878
    }
879
880
    /**
881
     * Generate markup for element type, with description suitable for use in
882
     * GridFields.
883
     *
884
     * @return DBField
885
     */
886
    public function getTypeNice()
887
    {
888
        $description = $this->getDescription();
889
        $desc = ($description) ? ' <span class="element__note"> &mdash; ' . $description . '</span>' : '';
890
891
        return DBField::create_field(
892
            'HTMLVarchar',
893
            $this->getType() . $desc
894
        );
895
    }
896
897
    /**
898
     * @return \SilverStripe\ORM\FieldType\DBHTMLText
899
     */
900
    public function getEditorPreview()
901
    {
902
        $templates = $this->getRenderTemplates('_EditorPreview');
903
        $templates[] = BaseElement::class . '_EditorPreview';
904
905
        return $this->renderWith($templates);
906
    }
907
908
    /**
909
     * @return Member
910
     */
911
    public function getAuthor()
912
    {
913
        if ($this->AuthorID) {
0 ignored issues
show
Bug Best Practice introduced by Will Rossiter
The property AuthorID does not exist on DNADesign\Elemental\Models\BaseElement. Since you implemented __get, consider adding a @property annotation.
Loading history...
914
            return Member::get()->byId($this->AuthorID);
915
        }
916
917
        return null;
918
    }
919
920
    /**
921
     * Get a user defined style variant for this element, if available
922
     *
923
     * @return string
924
     */
925
    public function getStyleVariant()
926
    {
927
        $style = $this->Style;
928
        $styles = $this->config()->get('styles');
929
930
        if (isset($styles[$style])) {
931
            $style = strtolower($style);
932
        } else {
933
            $style = '';
934
        }
935
936
        $this->extend('updateStyleVariant', $style);
937
938
        return $style;
939
    }
940
941
    /**
942
     * @return mixed|null
943
     * @throws \Psr\Container\NotFoundExceptionInterface
944
     * @throws \SilverStripe\ORM\ValidationException
945
     */
946
    public function getPageTitle()
947
    {
948
        $page = $this->getPage();
949
950
        if ($page) {
951
            return $page->Title;
952
        }
953
954
        return null;
955
    }
956
957
    /**
958
     * @return boolean
959
     */
960
    public function First()
961
    {
962
        return ($this->Parent()->Elements()->first()->ID === $this->ID);
963
    }
964
965
    /**
966
     * @return boolean
967
     */
968
    public function Last()
969
    {
970
        return ($this->Parent()->Elements()->last()->ID === $this->ID);
971
    }
972
973
    /**
974
     * @return int
975
     */
976
    public function TotalItems()
977
    {
978
        return $this->Parent()->Elements()->count();
979
    }
980
981
    /**
982
     * Returns the position of the current element.
983
     *
984
     * @return int
985
     */
986
    public function Pos()
987
    {
988
        return ($this->Parent()->Elements()->filter('Sort:LessThan', $this->Sort)->count() + 1);
989
    }
990
991
    /**
992
     * @return string
993
     */
994
    public function EvenOdd()
995
    {
996
        $odd = (bool) ($this->Pos() % 2);
997
998
        return  ($odd) ? 'odd' : 'even';
999
    }
1000
}
1001