Completed
Pull Request — master (#71)
by John
01:50
created

ElementPageExtension   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 386
Duplicated Lines 12.18 %

Coupling/Cohesion

Components 1
Dependencies 3

Importance

Changes 0
Metric Value
wmc 57
lcom 1
cbo 3
dl 47
loc 386
rs 6.433
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
C updateCMSFields() 0 64 10
D getAvailableTypes() 35 46 10
C onBeforeWrite() 0 48 8
C onAfterDelete() 0 35 7
B supportsElemental() 0 20 7
A onAfterDuplicate() 6 17 4
A onAfterDuplicateToSubsite() 6 15 2
B onAfterPublish() 0 22 5
A onBeforeRollback() 0 21 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ElementPageExtension 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 ElementPageExtension, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * @package elemental
5
 */
6
class ElementPageExtension extends DataExtension
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
7
{
8
9
    /**
10
     * @config
11
     *
12
     * @var string $elements_title Title of the element in the CMS.
13
     */
14
    private static $elements_title = 'Content Blocks';
0 ignored issues
show
Unused Code introduced by
The property $elements_title is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
15
16
    /**
17
     * @config
18
     *
19
     * @var array $ignored_classes Classes to ignore adding elements too.
20
     */
21
    private static $ignored_classes = array();
0 ignored issues
show
Unused Code introduced by
The property $ignored_classes is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
22
23
    /**
24
     * @config
25
     *
26
     * @var boolean
27
     */
28
    private static $copy_element_content_to_contentfield = true;
0 ignored issues
show
Unused Code introduced by
The property $copy_element_content_to_contentfield is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
29
30
    /**
31
     * @config
32
     *
33
     * @var boolean
34
     */
35
    private static $clear_contentfield = false;
0 ignored issues
show
Unused Code introduced by
The property $clear_contentfield is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
36
37
    /**
38
     * @var array $db
39
     */
40
    private static $db = array();
0 ignored issues
show
Unused Code introduced by
The property $db is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
41
42
    /**
43
     * @var array $has_one
44
     */
45
    private static $has_one = array(
0 ignored issues
show
Unused Code introduced by
The property $has_one is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
46
        'ElementArea' => 'ElementalArea'
47
    );
48
49
    /**
50
     * Setup the CMS Fields
51
     *
52
     * @param FieldList
53
     */
54
    public function updateCMSFields(FieldList $fields)
55
    {
56
        if(!$this->supportsElemental()) {
57
            return;
58
        }
59
60
        // add an empty holder for content as some module explicitly use insert
61
        // after content.
62
        $fields->replaceField('Content', new LiteralField('Content', ''));
63
64
        $adder = new ElementalGridFieldAddNewMultiClass('buttons-before-left');
65
66
        $list = $this->getAvailableTypes();
67
        if($list) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $list of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
68
            $adder->setClasses($list);
69
        }
70
71
        $area = $this->owner->ElementArea();
72
        if ($this->owner->exists() && (!$area->exists() || !$area->isInDB())) {
73
            $area->write();
74
75
            $this->owner->ElementAreaID = $area->ID;
76
            $this->owner->write();
77
        }
78
79
        $gridField = GridField::create('ElementArea',
80
            Config::inst()->get('ElementPageExtension', 'elements_title'),
81
            $area->AllElements(),
82
            $config = GridFieldConfig_RelationEditor::create()
83
                ->removeComponentsByType('GridFieldAddNewButton')
84
                ->removeComponentsByType('GridFieldSortableHeader')
85
                ->removeComponentsByType('GridFieldDeleteAction')
86
                ->removeComponentsByType('GridFieldAddExistingAutocompleter')
87
                ->addComponent($autocomplete = new ElementalGridFieldAddExistingAutocompleter('buttons-before-right'))
88
                ->addComponent(new GridFieldTitleHeader())
89
                ->addComponent($adder)
90
                ->addComponent(new GridFieldSortableRows('Sort'))
91
        );
92
93
        if ($this->owner->canArchive()) {
94
            $config->addComponent(new ElementalGridFieldDeleteAction());
95
        }
96
97
        $searchList = BaseElement::get()->filter('AvailableGlobally', true);
98
        if($list) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $list of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
99
            $searchList = $searchList->filter('ClassName', array_keys($list));
100
        }
101
        $autocomplete->setSearchList($searchList);
102
103
        $autocomplete->setResultsFormat('($ID) $Title');
104
        $autocomplete->setSearchFields(array('ID', 'Title'));
105
106
        $config = $gridField->getConfig();
107
        $paginator = $config->getComponentByType('GridFieldPaginator');
108
        $paginator->setItemsPerPage(100);
109
110
        if ($this->owner instanceof SiteTree && $fields->findOrMakeTab('Root.Main')->fieldByName('Metadata')) {
0 ignored issues
show
Bug introduced by
The class SiteTree does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
111
            $fields->addFieldToTab('Root.Main', $gridField, 'Metadata');
112
        } else {
113
            $fields->addFieldToTab('Root.Main', $gridField);
114
        }
115
116
        return $fields;
117
    }
118
119
120
    /**
121
     * @return array
122
     */
123
    public function getAvailableTypes() {
124
        if (is_array($this->owner->config()->get('allowed_elements'))) {
125
            $list = $this->owner->config()->get('allowed_elements');
126
127 View Code Duplication
            if($this->owner->config()->get('sort_types_alphabetically') !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
128
                $sorted = array();
129
130
                foreach ($list as $class) {
131
                    $inst = singleton($class);
132
133
                    if ($inst->canCreate()) {
134
                        $sorted[$class] = singleton($class)->i18n_singular_name();
135
                    }
136
                }
137
138
                $list = $sorted;
139
                asort($list);
140
            }
141 View Code Duplication
        } else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
142
            $classes = ClassInfo::subclassesFor('BaseElement');
143
            $list = array();
144
            unset($classes['BaseElement']);
145
146
            $disallowedElements = (array) $this->owner->config()->get('disallowed_elements');
147
148
            if (!in_array('ElementVirtualLinked', $disallowedElements)) {
149
                array_push($disallowedElements, 'ElementVirtualLinked');
150
            }
151
152
            foreach ($classes as $class) {
153
                $inst = singleton($class);
154
155
                if (!in_array($class, $disallowedElements) && $inst->canCreate()) {
156
                    $list[$class] = singleton($class)->i18n_singular_name();
157
                }
158
            }
159
160
            asort($list);
161
        }
162
163
        if (method_exists($this->owner, 'sortElementalOptions')) {
164
            $this->owner->sortElementalOptions($list);
165
        }
166
167
        return $list;
168
    }
169
170
    /**
171
     * Make sure there is always a WidgetArea sidebar for adding widgets
172
     *
173
     */
174
    public function onBeforeWrite()
175
    {
176
        if(!$this->supportsElemental()) {
177
            return;
178
        }
179
180
        // enable theme in case elements are being rendered with templates stored in theme folder
181
        $originalThemeEnabled = Config::inst()->get('SSViewer', 'theme_enabled');
182
        Config::inst()->update('SSViewer', 'theme_enabled', true);
183
184
        if ($this->owner->hasMethod('ElementArea') && Config::inst()->get(__CLASS__, 'copy_element_content_to_contentfield')) {
185
            $elements = $this->owner->ElementArea();
186
187
            if (!$elements->isInDB()) {
188
                $elements->write();
189
                $this->owner->ElementAreaID = $elements->ID;
190
            } else {
191
                // Copy widgets content to Content to enable search
192
                $searchableContent = array();
193
194
                Requirements::clear();
195
196
                foreach ($elements->Elements() as $element) {
197
                    if ($element->config()->exclude_from_content) {
198
                        continue;
199
                    }
200
201
                    $controller = $element->getController();
202
                    $controller->init();
203
204
                    array_push($searchableContent, $controller->WidgetHolder());
205
                }
206
207
                Requirements::restore();
208
209
                $this->owner->Content = trim(implode(' ', $searchableContent));
210
            }
211
        } else {
212
            if(Config::inst()->get(__CLASS__, 'clear_contentfield')) {
213
                $this->owner->Content = '';
214
            }
215
        }
216
217
        // set theme_enabled back to what it was
218
        Config::inst()->update('SSViewer', 'theme_enabled', $originalThemeEnabled);
219
220
        parent::onBeforeWrite();
221
    }
222
223
    /**
224
     * Ensure that if there are elements that belong to this page
225
     * and are virtualised (Virtual Element links to them), that we move the
226
     * original element to replace one of the virtual elements
227
     * But only if it's a delete not an unpublish
228
     */
229
    public function onAfterDelete() {
230
        if(Versioned::get_reading_mode() == 'Stage.Stage') {
231
            $area = $this->owner->ElementArea();
232
            foreach ($area->Widgets() as $element) {
233
                $firstVirtual = false;
234
                if ($element->getPublishedVirtualLinkedElements()->Count() > 0) {
235
                    // choose the first one
236
                    $firstVirtual = $element->getPublishedVirtualLinkedElements()->First();
237
                    $wasPublished = true;
238
                } else if ($element->getVirtualLinkedElements()->Count() > 0) {
239
                    // choose the first one
240
                    $firstVirtual = $element->getVirtualLinkedElements()->First();
241
                    $wasPublished = false;
242
                }
243
                if ($firstVirtual) {
244
                    $origParentID = $element->ParentID;
245
                    $origSort = $element->Sort;
246
247
                    // change element to first's values
248
                    $element->ParentID = $firstVirtual->ParentID;
249
                    $element->Sort = $firstVirtual->Sort;
250
251
                    $firstVirtual->ParentID = $origParentID;
252
                    $firstVirtual->Sort = $origSort;
253
                    // write
254
                    $element->write();
255
                    $firstVirtual->write();
256
                    if ($wasPublished) {
0 ignored issues
show
Bug introduced by
The variable $wasPublished does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
257
                        $element->doPublish();
258
                        $firstVirtual->doPublish();
259
                    }
260
                }
261
            }
262
        }
263
    }
264
265
    /**
266
     * @return boolean
267
     */
268
    public function supportsElemental() {
269
        if ($this->owner->hasMethod('includeElemental')) {
270
            $res = $this->owner->includeElemental();
271
            if ($res !== null) {
272
                return $res;
273
            }
274
        }
275
276
        if (is_a($this->owner, 'RedirectorPage')) {
277
            return false;
278
        } else if ($ignored = Config::inst()->get('ElementPageExtension', 'ignored_classes')) {
279
            foreach ($ignored as $check) {
280
                if (is_a($this->owner, $check)) {
281
                    return false;
282
                }
283
            }
284
        }
285
286
        return true;
287
    }
288
289
    /**
290
     * If the page is duplicated, copy the widgets across too.
291
     *
292
     * Gets called twice from either direction, due to bad DataObject and SiteTree code, hence the weird if statement
293
     *
294
     * @return Page The duplicated page
0 ignored issues
show
Documentation introduced by
Should the return type not be Page|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
295
     */
296
    public function onAfterDuplicate($duplicatePage)
297
    {
298
        if ($this->owner->ID != 0 && $this->owner->ID < $duplicatePage->ID) {
299
            $originalWidgetArea = $this->owner->getComponent('ElementArea');
300
            $duplicateWidgetArea = $originalWidgetArea->duplicate(false);
301
            $duplicateWidgetArea->write();
302
            $duplicatePage->ElementAreaID = $duplicateWidgetArea->ID;
303
            $duplicatePage->write();
304
305 View Code Duplication
            foreach ($originalWidgetArea->Items() as $originalWidget) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
306
                $duplicateWidget = $originalWidget->duplicate(true);
307
308
                // manually set the ParentID of each widget, so we don't get versioning issues
309
                DB::query(sprintf("UPDATE Widget SET ParentID = %d WHERE ID = %d", $duplicateWidgetArea->ID, $duplicateWidget->ID));
310
            }
311
        }
312
    }
313
314
    /**
315
     * If the page is duplicated across subsites, copy the widgets across too.
316
     *
317
     * @return Page The duplicated page
0 ignored issues
show
Documentation introduced by
Should the return type not be Page|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
318
     */
319
    public function onAfterDuplicateToSubsite($originalPage)
320
    {
321
        $originalWidgetArea = $originalPage->getComponent('ElementArea');
322
        $duplicateWidgetArea = $originalWidgetArea->duplicate(false);
323
        $duplicateWidgetArea->write();
324
        $this->owner->ElementAreaID = $duplicateWidgetArea->ID;
325
        $this->owner->write();
326
327 View Code Duplication
        foreach ($originalWidgetArea->Items() as $originalWidget) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
328
            $duplicateWidget = $originalWidget->duplicate(true);
329
330
            // manually set the ParentID of each widget, so we don't get versioning issues
331
            DB::query(sprintf("UPDATE Widget SET ParentID = %d WHERE ID = %d", $duplicateWidgetArea->ID, $duplicateWidget->ID));
332
        }
333
    }
334
335
    /**
336
     * Publish
337
     */
338
    public function onAfterPublish()
339
    {
340
        if ($id = $this->owner->ElementAreaID) {
341
            $widgets = Versioned::get_by_stage('BaseElement', 'Stage', "ParentID = '$id'");
342
            $staged = array();
343
344
            foreach ($widgets as $widget) {
345
                $staged[] = $widget->ID;
346
347
                $widget->publish('Stage', 'Live');
348
            }
349
350
            // remove any elements that are on live but not in draft.
351
            $widgets = Versioned::get_by_stage('BaseElement', 'Live', "ParentID = '$id'");
352
353
            foreach ($widgets as $widget) {
354
                if (!in_array($widget->ID, $staged)) {
355
                    $widget->deleteFromStage('Live');
356
                }
357
            }
358
        }
359
    }
360
361
    /**
362
     * Roll back all changes if the parent page has a rollback event
363
     *
364
     * Only do rollback if it's the 'cancel draft changes' rollback, not a specific version
365
     * rollback.
366
     *
367
     * @param string $version
368
     * @return null
369
     */
370
    public function onBeforeRollback($version)
371
    {
372
        if ($version !== 'Live') {
373
            // we don't yet have a smart way of rolling back to a specific version
374
            return;
375
        }
376
        if ($id = $this->owner->ElementAreaID) {
377
            $widgets = Versioned::get_by_stage('BaseElement', 'Live', "ParentID = '$id'");
378
            $staged = array();
379
380
            foreach ($widgets as $widget) {
381
                $staged[] = $widget->ID;
382
383
                $widget->invokeWithExtensions('onBeforeRollback', $widget);
384
385
                $widget->publish('Live', 'Stage', false);
386
387
                $widget->invokeWithExtensions('onAfterRollback', $widget);
388
            }
389
        }
390
    }
391
}
392