Passed
Pull Request — 4 (#930)
by Steve
04:01
created

BaseElement   F

Complexity

Total Complexity 121

Size/Duplication

Total Lines 1035
Duplicated Lines 0 %

Importance

Changes 17
Bugs 0 Features 0
Metric Value
eloc 336
c 17
b 0
f 0
dl 0
loc 1035
rs 2
wmc 121

49 Methods

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

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\TextCheckboxGroupField;
7
use DNADesign\Elemental\ORM\FieldType\DBObjectType;
8
use Exception;
9
use SilverStripe\CMS\Controllers\CMSPageEditController;
10
use SilverStripe\CMS\Model\SiteTree;
11
use SilverStripe\Control\Controller;
12
use SilverStripe\Control\Director;
13
use SilverStripe\Core\ClassInfo;
14
use SilverStripe\Core\Injector\Injector;
15
use SilverStripe\Forms\DropdownField;
16
use SilverStripe\Forms\FieldList;
17
use SilverStripe\Forms\HiddenField;
18
use SilverStripe\Forms\NumericField;
19
use SilverStripe\Forms\TextField;
20
use SilverStripe\GraphQL\Scaffolding\StaticSchema;
21
use SilverStripe\GraphQL\Schema\Exception\SchemaBuilderException;
0 ignored issues
show
Bug introduced by
The type SilverStripe\GraphQL\Sch...\SchemaBuilderException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
22
use SilverStripe\GraphQL\Schema\Schema;
0 ignored issues
show
Bug introduced by
The type SilverStripe\GraphQL\Schema\Schema was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
23
use SilverStripe\GraphQL\Schema\SchemaBuilder;
0 ignored issues
show
Bug introduced by
The type SilverStripe\GraphQL\Schema\SchemaBuilder was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
324
                ->setTitle(_t(__CLASS__ . '.SettingsTabLabel', 'Settings'));
325
326
            // Add a combined field for "Title" and "Displayed" checkbox in a Bootstrap input group
327
            $fields->removeByName('ShowTitle');
328
329
            if ($this->config()->get('displays_title_in_template')) {
330
                $fields->replaceField(
331
                    'Title',
332
                    TextCheckboxGroupField::create()
333
                        ->setName('Title')
334
                );
335
            }
336
337
            // Rename the "Main" tab
338
            $fields->fieldByName('Root.Main')
0 ignored issues
show
Bug introduced by
Are you sure the usage of $fields->fieldByName('Root.Main') targeting SilverStripe\Forms\FieldList::fieldByName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
339
                ->setTitle(_t(__CLASS__ . '.MainTabLabel', 'Content'));
340
341
            $fields->addFieldsToTab('Root.Main', [
342
                HiddenField::create('AbsoluteLink', false, Director::absoluteURL($this->PreviewLink())),
343
                HiddenField::create('LiveLink', false, Director::absoluteURL($this->Link())),
344
                HiddenField::create('StageLink', false, Director::absoluteURL($this->PreviewLink())),
345
            ]);
346
347
            $styles = $this->config()->get('styles');
348
349
            if ($styles && count($styles) > 0) {
350
                $styleDropdown = DropdownField::create('Style', _t(__CLASS__.'.STYLE', 'Style variation'), $styles);
351
352
                $fields->insertBefore($styleDropdown, 'ExtraClass');
0 ignored issues
show
Bug introduced by
'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

352
                $fields->insertBefore($styleDropdown, /** @scrutinizer ignore-type */ 'ExtraClass');
Loading history...
353
354
                $styleDropdown->setEmptyString(_t(__CLASS__.'.CUSTOM_STYLES', 'Select a style..'));
355
            } else {
356
                $fields->removeByName('Style');
357
            }
358
359
            // Hide the navigation section of the tabs in the React component {@see silverstripe/admin Tabs}
360
            $rootTabset = $fields->fieldByName('Root');
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $rootTabset is correct as $fields->fieldByName('Root') targeting SilverStripe\Forms\FieldList::fieldByName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
361
            $rootTabset->setSchemaState(['hideNav' => true]);
362
363
            if ($this->isInDB()) {
364
                $fields->addFieldsToTab('Root.History', [
365
                    HistoryViewerField::create('ElementHistory'),
366
                ]);
367
                // Add class to containing tab
368
                $fields->fieldByName('Root.History')
0 ignored issues
show
Bug introduced by
Are you sure the usage of $fields->fieldByName('Root.History') targeting SilverStripe\Forms\FieldList::fieldByName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
369
                    ->addExtraClass('elemental-block__history-tab tab--history-viewer');
370
371
                // Hack: automatically navigate to the History tab with `#Root_History` is in the URL
372
                // To unhack, fix this: https://github.com/silverstripe/silverstripe-admin/issues/911
373
                Requirements::customScript(<<<JS
374
    document.addEventListener('DOMContentLoaded', () => {
375
        var hash = window.location.hash.substr(1);
376
        if (hash !== 'Root_History') {
377
            return null;
378
        }
379
        jQuery('.cms-tabset-nav-primary li[aria-controls="Root_History"] a').trigger('click')
380
    });
381
JS
382
                );
383
            }
384
        });
385
386
        return parent::getCMSFields();
387
    }
388
389
    /**
390
     * Get the type of the current block, for use in GridField summaries, block
391
     * type dropdowns etc. Examples are "Content", "File", "Media", etc.
392
     *
393
     * @return string
394
     */
395
    public function getType()
396
    {
397
        return _t(__CLASS__ . '.BlockType', 'Block');
398
    }
399
400
    /**
401
     * Proxy through to configuration setting 'inline_editable'
402
     *
403
     * @return bool
404
     */
405
    public function inlineEditable()
406
    {
407
        return static::config()->get('inline_editable');
408
    }
409
410
    /**
411
     * @param ElementController $controller
412
     *
413
     * @return $this
414
     */
415
    public function setController($controller)
416
    {
417
        $this->controller = $controller;
418
419
        return $this;
420
    }
421
422
    /**
423
     * @throws Exception If the specified controller class doesn't exist
424
     *
425
     * @return ElementController
426
     */
427
    public function getController()
428
    {
429
        if ($this->controller) {
430
            return $this->controller;
431
        }
432
433
        $controllerClass = self::config()->controller_class;
434
435
        if (!class_exists($controllerClass)) {
436
            throw new Exception(
437
                'Could not find controller class ' . $controllerClass . ' as defined in ' . static::class
438
            );
439
        }
440
441
        $this->controller = Injector::inst()->create($controllerClass, $this);
442
        $this->controller->doInit();
443
444
        return $this->controller;
445
    }
446
447
    /**
448
     * @param string $name
449
     * @return $this
450
     */
451
    public function setAreaRelationNameCache($name)
452
    {
453
        $this->cacheData['area_relation_name'] = $name;
454
455
        return $this;
456
    }
457
458
    /**
459
     * @return Controller
460
     */
461
    public function Top()
462
    {
463
        return (Controller::has_curr()) ? Controller::curr() : null;
464
    }
465
466
    /**
467
     * Default way to render element in templates. Note that all blocks should
468
     * be rendered through their {@link ElementController} class as this
469
     * contains the holder styles.
470
     *
471
     * @return string|null HTML
472
     */
473
    public function forTemplate($holder = true)
474
    {
475
        $templates = $this->getRenderTemplates();
476
477
        if ($templates) {
478
            return $this->renderWith($templates);
479
        }
480
481
        return null;
482
    }
483
484
    /**
485
     * @param string $suffix
486
     *
487
     * @return array
488
     */
489
    public function getRenderTemplates($suffix = '')
490
    {
491
        $classes = ClassInfo::ancestry($this->ClassName);
492
        $classes[static::class] = static::class;
493
        $classes = array_reverse($classes);
494
        $templates = [];
495
496
        foreach ($classes as $key => $class) {
497
            if ($class == BaseElement::class) {
498
                continue;
499
            }
500
501
            if ($class == DataObject::class) {
502
                break;
503
            }
504
505
            if ($style = $this->Style) {
506
                $templates[$class][] = $class . $suffix . '_'. $this->getAreaRelationName() . '_' . $style;
507
                $templates[$class][] = $class . $suffix . '_' . $style;
508
            }
509
            $templates[$class][] = $class . $suffix . '_'. $this->getAreaRelationName();
510
            $templates[$class][] = $class . $suffix;
511
        }
512
513
        $this->extend('updateRenderTemplates', $templates, $suffix);
514
515
        $templateFlat = [];
516
        foreach ($templates as $class => $variations) {
517
            $templateFlat = array_merge($templateFlat, $variations);
518
        }
519
520
        return $templateFlat;
521
    }
522
523
    /**
524
     * Given form data (wit
525
     *
526
     * @param $data
527
     */
528
    public function updateFromFormData($data)
529
    {
530
        $cmsFields = $this->getCMSFields();
531
532
        foreach ($data as $field => $datum) {
533
            $field = $cmsFields->dataFieldByName($field);
534
535
            if (!$field) {
536
                continue;
537
            }
538
539
            $field->setSubmittedValue($datum);
540
            $field->saveInto($this);
541
        }
542
    }
543
544
    /**
545
     * Strip all namespaces from class namespace.
546
     *
547
     * @param string $classname e.g. "\Fully\Namespaced\Class"
548
     *
549
     * @return string following the param example, "Class"
550
     */
551
    protected function stripNamespacing($classname)
552
    {
553
        $classParts = explode('\\', $classname);
554
        return array_pop($classParts);
555
    }
556
557
    /**
558
     * @return string
559
     */
560
    public function getSimpleClassName()
561
    {
562
        return strtolower($this->sanitiseClassName($this->ClassName, '__'));
563
    }
564
565
    /**
566
     * @return null|SiteTree
567
     * @throws \Psr\Container\NotFoundExceptionInterface
568
     * @throws \SilverStripe\ORM\ValidationException
569
     */
570
    public function getPage()
571
    {
572
        // Allow for repeated calls to be cached
573
        if (isset($this->cacheData['page'])) {
574
            return $this->cacheData['page'];
575
        }
576
577
578
        $class = DataObject::getSchema()->hasOneComponent($this, 'Parent');
579
        $area = ($this->ParentID) ? DataObject::get_by_id($class, $this->ParentID) : null;
580
        if ($area instanceof ElementalArea && $area->exists()) {
581
            $this->cacheData['page'] = $area->getOwnerPage();
582
            return $this->cacheData['page'];
583
        }
584
585
        return null;
586
    }
587
588
    /**
589
     * Get a unique anchor name
590
     *
591
     * @return string
592
     */
593
    public function getAnchor()
594
    {
595
        if ($this->anchor !== null) {
596
            return $this->anchor;
597
        }
598
599
        $anchorTitle = '';
600
601
        if (!$this->config()->disable_pretty_anchor_name) {
602
            if ($this->hasMethod('getAnchorTitle')) {
603
                $anchorTitle = $this->getAnchorTitle();
0 ignored issues
show
Bug introduced by
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

603
                /** @scrutinizer ignore-call */ 
604
                $anchorTitle = $this->getAnchorTitle();
Loading history...
604
            } elseif ($this->config()->enable_title_in_template) {
605
                $anchorTitle = $this->getField('Title');
606
            }
607
        }
608
609
        if (!$anchorTitle) {
610
            $anchorTitle = 'e'.$this->ID;
611
        }
612
613
        $filter = URLSegmentFilter::create();
614
        $titleAsURL = $filter->filter($anchorTitle);
615
616
        // Ensure that this anchor name isn't already in use
617
        // ie. If two elemental blocks have the same title, it'll append '-2', '-3'
618
        $result = $titleAsURL;
619
        $count = 1;
620
        while (isset(self::$used_anchors[$result]) && self::$used_anchors[$result] !== $this->ID) {
621
            ++$count;
622
            $result = $titleAsURL . '-' . $count;
623
        }
624
        self::$used_anchors[$result] = $this->ID;
625
        return $this->anchor = $result;
626
    }
627
628
    /**
629
     * @param string|null $action
630
     * @return string|null
631
     * @throws \Psr\Container\NotFoundExceptionInterface
632
     * @throws \SilverStripe\ORM\ValidationException
633
     */
634
    public function AbsoluteLink($action = null)
635
    {
636
        if ($page = $this->getPage()) {
637
            $link = $page->AbsoluteLink($action) . '#' . $this->getAnchor();
638
639
            $this->extend('updateAbsoluteLink', $link);
640
641
            return $link;
642
        }
643
644
        return null;
645
    }
646
647
    /**
648
     * @param string|null $action
649
     * @return string
650
     * @throws \Psr\Container\NotFoundExceptionInterface
651
     * @throws \SilverStripe\ORM\ValidationException
652
     */
653
    public function Link($action = null)
654
    {
655
        if ($page = $this->getPage()) {
656
            $link = $page->Link($action) . '#' . $this->getAnchor();
657
658
            $this->extend('updateLink', $link);
659
660
            return $link;
661
        }
662
663
        return null;
664
    }
665
666
    /**
667
     * @param string|null $action
668
     * @return string
669
     * @throws \Psr\Container\NotFoundExceptionInterface
670
     * @throws \SilverStripe\ORM\ValidationException
671
     */
672
    public function PreviewLink($action = null)
673
    {
674
        $action = $action . '?ElementalPreview=' . mt_rand();
675
        $link = $this->Link($action);
676
        $this->extend('updatePreviewLink', $link);
677
678
        return $link;
679
    }
680
681
    /**
682
     * @return boolean
683
     */
684
    public function isCMSPreview()
685
    {
686
        if (Controller::has_curr()) {
687
            $controller = Controller::curr();
688
689
            if ($controller->getRequest()->requestVar('CMSPreview')) {
690
                return true;
691
            }
692
        }
693
694
        return false;
695
    }
696
697
    /**
698
     * @param bool $directLink Indicates that the GridFieldDetailEdit form link should be given even if the block can be
699
     *                         edited in-line.
700
     * @return null|string
701
     * @throws \SilverStripe\ORM\ValidationException
702
     */
703
    public function CMSEditLink($directLink = false)
704
    {
705
        // Allow for repeated calls to be returned from cache
706
        if (isset($this->cacheData['cms_edit_link'])) {
707
            return $this->cacheData['cms_edit_link'];
708
        }
709
710
        $relationName = $this->getAreaRelationName();
711
        $page = $this->getPage();
712
713
        if (!$page) {
714
            $link = null;
715
            $this->extend('updateCMSEditLink', $link);
716
            return $link;
717
        }
718
719
        if (!$page instanceof SiteTree && method_exists($page, 'CMSEditLink')) {
0 ignored issues
show
introduced by
$page is always a sub-type of SilverStripe\CMS\Model\SiteTree.
Loading history...
720
            $link = Controller::join_links($page->CMSEditLink(), 'ItemEditForm');
721
        } else {
722
            $link = $page->CMSEditLink();
723
        }
724
725
        // In-line editable blocks should just take you to the page. Editable ones should add the suffix for detail form
726
        if (!$this->inlineEditable() || $directLink) {
727
            $link = Controller::join_links(
728
                singleton(CMSPageEditController::class)->Link('EditForm'),
729
                $page->ID,
730
                'field/' . $relationName . '/item/',
731
                $this->ID,
732
                'edit'
733
            );
734
        }
735
736
        $this->extend('updateCMSEditLink', $link);
737
738
        $this->cacheData['cms_edit_link'] = $link;
739
        return $link;
740
    }
741
742
    /**
743
     * Retrieve a elemental area relation for creating cms links
744
     *
745
     * @return int|string The name of a valid elemental area relation
746
     * @throws \Psr\Container\NotFoundExceptionInterface
747
     * @throws \SilverStripe\ORM\ValidationException
748
     */
749
    public function getAreaRelationName()
750
    {
751
        // Allow repeated calls to return from internal cache
752
        if (isset($this->cacheData['area_relation_name'])) {
753
            return $this->cacheData['area_relation_name'];
754
        }
755
756
        $page = $this->getPage();
757
758
        $result = 'ElementalArea';
759
760
        if ($page) {
761
            $has_one = $page->config()->get('has_one');
762
            $area = $this->Parent();
763
764
            foreach ($has_one as $relationName => $relationClass) {
765
                if ($page instanceof BaseElement && $relationName === 'Parent') {
766
                    continue;
767
                }
768
                if ($relationClass === $area->ClassName && $page->{$relationName}()->ID === $area->ID) {
769
                    $result = $relationName;
770
                    break;
771
                }
772
            }
773
        }
774
775
        $this->setAreaRelationNameCache($result);
776
777
        return $result;
778
    }
779
780
    /**
781
     * Sanitise a model class' name for inclusion in a link.
782
     *
783
     * @return string
784
     */
785
    public function sanitiseClassName($class, $delimiter = '-')
786
    {
787
        return str_replace('\\', $delimiter, $class);
788
    }
789
790
    public function unsanitiseClassName($class, $delimiter = '-')
791
    {
792
        return str_replace($delimiter, '\\', $class);
793
    }
794
795
    /**
796
     * @return null|string
797
     * @throws \Psr\Container\NotFoundExceptionInterface
798
     * @throws \SilverStripe\ORM\ValidationException
799
     */
800
    public function getEditLink()
801
    {
802
        return Director::absoluteURL($this->CMSEditLink());
803
    }
804
805
    /**
806
     * @return DBField|null
807
     * @throws \Psr\Container\NotFoundExceptionInterface
808
     * @throws \SilverStripe\ORM\ValidationException
809
     */
810
    public function PageCMSEditLink()
811
    {
812
        if ($page = $this->getPage()) {
813
            return DBField::create_field('HTMLText', sprintf(
814
                '<a href="%s">%s</a>',
815
                $page->CMSEditLink(),
816
                $page->Title
817
            ));
818
        }
819
820
        return null;
821
    }
822
823
    /**
824
     * @return string
825
     */
826
    public function getMimeType()
827
    {
828
        return 'text/html';
829
    }
830
831
    /**
832
     * This can be overridden on child elements to create a summary for display
833
     * in GridFields.
834
     *
835
     * @return string
836
     */
837
    public function getSummary()
838
    {
839
        return '';
840
    }
841
842
    /**
843
     * The block config defines a set of data (usually set through config on the element) that will be made available in
844
     * client side config. Individual element types may choose to add config variable for use in React code
845
     *
846
     * @return array
847
     */
848
    public static function getBlockConfig()
849
    {
850
        return [];
851
    }
852
853
    /**
854
     * The block actions is an associative array available for providing data to the client side to be used to describe
855
     * actions that may be performed. This is available as a plain "ObjectType" in the GraphQL schema.
856
     *
857
     * By default the only action is "edit" which is simply the URL where the block may be edited.
858
     *
859
     * To modify the actions, either use the extension point or overload the `provideBlockSchema` method.
860
     *
861
     * @internal This API may change in future. Treat this as a `final` method.
862
     * @return array
863
     */
864
    public function getBlockSchema()
865
    {
866
        $blockSchema = $this->provideBlockSchema();
867
868
        $this->extend('updateBlockSchema', $blockSchema);
869
870
        return $blockSchema;
871
    }
872
873
    /**
874
     * Provide block schema data, which will be serialised and sent via GraphQL to the editor client.
875
     *
876
     * Overload this method in child element classes to augment, or use the extension point on `getBlockSchema`
877
     * to update it from an `Extension`.
878
     *
879
     * @return array
880
     * @throws SchemaBuilderException
881
     * @throws ValidationException
882
     */
883
    protected function provideBlockSchema()
884
    {
885
        return [
886
            'typeName' => static::getGraphQLTypeName(),
887
            'actions' => [
888
                'edit' => $this->getEditLink(),
889
            ],
890
        ];
891
    }
892
893
    /**
894
     * Generate markup for element type icons suitable for use in GridFields.
895
     *
896
     * @return null|DBHTMLText
897
     */
898
    public function getIcon()
899
    {
900
        $data = ArrayData::create([]);
901
902
        $iconClass = $this->config()->get('icon');
903
        if ($iconClass) {
904
            $data->IconClass = $iconClass;
905
906
            // Add versioned states (rendered as a circle over the icon)
907
            if ($this->hasExtension(Versioned::class)) {
908
                $data->IsVersioned = true;
909
                if ($this->isOnDraftOnly()) {
0 ignored issues
show
Bug introduced by
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

909
                if ($this->/** @scrutinizer ignore-call */ isOnDraftOnly()) {
Loading history...
910
                    $data->VersionState = 'draft';
911
                    $data->VersionStateTitle = _t(
912
                        'SilverStripe\\Versioned\\VersionedGridFieldState\\VersionedGridFieldState.ADDEDTODRAFTHELP',
913
                        'Item has not been published yet'
914
                    );
915
                } elseif ($this->isModifiedOnDraft()) {
0 ignored issues
show
Bug introduced by
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

915
                } elseif ($this->/** @scrutinizer ignore-call */ isModifiedOnDraft()) {
Loading history...
916
                    $data->VersionState = 'modified';
917
                    $data->VersionStateTitle = $data->VersionStateTitle = _t(
918
                        'SilverStripe\\Versioned\\VersionedGridFieldState\\VersionedGridFieldState.MODIFIEDONDRAFTHELP',
919
                        'Item has unpublished changes'
920
                    );
921
                }
922
            }
923
924
            return $data->renderWith(__CLASS__ . '/PreviewIcon');
925
        }
926
927
        return null;
928
    }
929
930
    /**
931
     * Get a description for this content element, if available
932
     *
933
     * @return string
934
     */
935
    public function getDescription()
936
    {
937
        $description = $this->config()->uninherited('description');
938
        if ($description) {
939
            return _t(__CLASS__ . '.Description', $description);
940
        }
941
        return '';
942
    }
943
944
    /**
945
     * Generate markup for element type, with description suitable for use in
946
     * GridFields.
947
     *
948
     * @return DBField
949
     */
950
    public function getTypeNice()
951
    {
952
        $description = $this->getDescription();
953
        $desc = ($description) ? ' <span class="element__note"> &mdash; ' . $description . '</span>' : '';
954
955
        return DBField::create_field(
956
            'HTMLVarchar',
957
            $this->getType() . $desc
958
        );
959
    }
960
961
    /**
962
     * @return \SilverStripe\ORM\FieldType\DBHTMLText
963
     */
964
    public function getEditorPreview()
965
    {
966
        $templates = $this->getRenderTemplates('_EditorPreview');
967
        $templates[] = BaseElement::class . '_EditorPreview';
968
969
        return $this->renderWith($templates);
970
    }
971
972
    /**
973
     * @return Member
974
     */
975
    public function getAuthor()
976
    {
977
        if ($this->AuthorID) {
0 ignored issues
show
Bug Best Practice introduced by
The property AuthorID does not exist on DNADesign\Elemental\Models\BaseElement. Since you implemented __get, consider adding a @property annotation.
Loading history...
978
            return Member::get()->byId($this->AuthorID);
979
        }
980
981
        return null;
982
    }
983
984
    /**
985
     * Get a user defined style variant for this element, if available
986
     *
987
     * @return string
988
     */
989
    public function getStyleVariant()
990
    {
991
        $style = $this->Style;
992
        $styles = $this->config()->get('styles');
993
994
        if (isset($styles[$style])) {
995
            $style = strtolower($style);
996
        } else {
997
            $style = '';
998
        }
999
1000
        $this->extend('updateStyleVariant', $style);
1001
1002
        return $style;
1003
    }
1004
1005
    /**
1006
     * @return mixed|null
1007
     * @throws \Psr\Container\NotFoundExceptionInterface
1008
     * @throws \SilverStripe\ORM\ValidationException
1009
     */
1010
    public function getPageTitle()
1011
    {
1012
        $page = $this->getPage();
1013
1014
        if ($page) {
1015
            return $page->Title;
1016
        }
1017
1018
        return null;
1019
    }
1020
1021
    /**
1022
     * @return boolean
1023
     */
1024
    public function First()
1025
    {
1026
        return ($this->Parent()->Elements()->first()->ID === $this->ID);
1027
    }
1028
1029
    /**
1030
     * @return boolean
1031
     */
1032
    public function Last()
1033
    {
1034
        return ($this->Parent()->Elements()->last()->ID === $this->ID);
1035
    }
1036
1037
    /**
1038
     * @return int
1039
     */
1040
    public function TotalItems()
1041
    {
1042
        return $this->Parent()->Elements()->count();
1043
    }
1044
1045
    /**
1046
     * Returns the position of the current element.
1047
     *
1048
     * @return int
1049
     */
1050
    public function Pos()
1051
    {
1052
        return ($this->Parent()->Elements()->filter('Sort:LessThan', $this->Sort)->count() + 1);
1053
    }
1054
1055
    /**
1056
     * @return string
1057
     */
1058
    public function EvenOdd()
1059
    {
1060
        $odd = (bool) ($this->Pos() % 2);
1061
1062
        return  ($odd) ? 'odd' : 'even';
1063
    }
1064
1065
    /**
1066
     * @return string
1067
     */
1068
    public static function getGraphQLTypeName(): string
1069
    {
1070
        return class_exists(StaticSchema::class)
1071
            ? StaticSchema::inst()->typeNameForDataObject(static::class)
1072
            : str_replace('\\', '_', static::class);
1073
    }
1074
1075
    public function validate()
1076
    {
1077
        /** @var ValidationResult $result */
1078
        $result = parent::validate(); // DataObject::validate()
1079
        if (empty($this->Title)) {  // sboyd
1080
            $result->addFieldError('Title', 'Is empty!!!!');
1081
        }
1082
        return $result;
1083
    }
1084
1085
    public function getCMSValidator()
1086
    {
1087
        return new RequiredFields(['Title']);
1088
    }
1089
}
1090