Completed
Push — master ( 7b3b2c...a43b11 )
by Will
02:51
created

ElementPageExtension   C

Complexity

Total Complexity 42

Size/Duplication

Total Lines 295
Duplicated Lines 8.47 %

Coupling/Cohesion

Components 1
Dependencies 17

Importance

Changes 32
Bugs 9 Features 4
Metric Value
wmc 42
c 32
b 9
f 4
lcom 1
cbo 17
dl 25
loc 295
rs 6.5405

8 Methods

Rating   Name   Duplication   Size   Complexity  
B updateCMSFields() 0 56 9
B getAvailableTypes() 13 25 5
C onBeforeWrite() 0 46 7
B supportsElemental() 0 17 6
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
     * @var array $db
25
     */
26
    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...
27
28
    /**
29
     * @var array $has_one
30
     */
31
    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...
32
        'ElementArea' => 'ElementalArea'
33
    );
34
35
    /**
36
     * Setup the CMS Fields
37
     *
38
     * @param FieldList
39
     */
40
    public function updateCMSFields(FieldList $fields)
41
    {
42
        if(!$this->supportsElemental()) {
43
            return false;
44
        }
45
46
        // add an empty holder for content as some module explicitly use insert
47
        // after content.
48
        $fields->replaceField('Content', new LiteralField('Content', ''));
49
50
        $adder = new ElementalGridFieldAddNewMultiClass();
51
52
        $list = $this->getAvailableTypes();
53
        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...
54
            $adder->setClasses($list);
55
        }
56
57
        $area = $this->owner->ElementArea();
58
59
        if ($this->owner->exists() && (!$area->exists() || !$area->isInDB())) {
60
            $area->write();
61
62
            $this->owner->ElementAreaID = $area->ID;
63
            $this->owner->write();
64
        }
65
66
        $elements = $this->owner->ElementArea()->Elements();
67
        if (!$elements || $elements instanceof ArrayList || $elements instanceof UnsavedRelationList) {
68
            // Allow gridfield to render on an unsaved DataObject
69
            $elements = new UnsavedRelationList('ElementalArea', 'Widgets', 'BaseElement');
70
        }
71
72
        $gridField = GridField::create('ElementArea',
73
            Config::inst()->get("ElementPageExtension", 'elements_title'),
74
            $elements,
75
            GridFieldConfig_RelationEditor::create()
76
                ->removeComponentsByType('GridFieldAddNewButton')
77
                ->removeComponentsByType('GridFieldDeleteAction')
78
                ->removeComponentsByType('GridFieldAddExistingAutocompleter')
79
                ->addComponent(new ElementalGridFieldAddExistingAutocompleter())
80
                ->addComponent(new ElementalGridFieldDeleteAction())
81
                ->addComponent($adder)
82
                ->addComponent(new GridFieldSortableRows('Sort'))
83
        );
84
85
        $config = $gridField->getConfig();
86
        $paginator = $config->getComponentByType('GridFieldPaginator');
87
        $paginator->setItemsPerPage(100);
88
89
        $config->removeComponentsByType('GridFieldDetailForm');
90
        $config->addComponent(new VersionedDataObjectDetailsForm());
91
92
        $fields->addFieldToTab('Root.Main', $gridField);
93
94
        return $fields;
95
    }
96
97
    /**
98
     * @return array
99
     */
100
    public function getAvailableTypes() {
101
        if (is_array($this->owner->config()->get('allowed_elements'))) {
102
            $list = $this->owner->config()->get('allowed_elements');
103 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...
104
            $classes = ClassInfo::subclassesFor('BaseElement');
105
            $list = array();
106
            unset($classes['BaseElement']);
107
108
            foreach ($classes as $class) {
0 ignored issues
show
Bug introduced by
The expression $classes of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
109
                $inst = singleton($class);
110
111
                if ($inst->canCreate()) {
112
                    $list[$class] = singleton($class)->i18n_singular_name();
113
                }
114
            }
115
        }
116
117
        if (method_exists($this->owner, 'sortElementalOptions')) {
118
            $this->owner->sortElementalOptions($list);
119
        } else {
120
            asort($list);
121
        }
122
123
        return $list;
124
    }
125
126
    /**
127
     * Make sure there is always a WidgetArea sidebar for adding widgets
128
     *
129
     */
130
    public function onBeforeWrite()
131
    {
132
        // enable theme in case elements are being rendered with templates stored in theme folder
133
        $originalThemeEnabled = Config::inst()->get('SSViewer', 'theme_enabled');
134
        Config::inst()->update('SSViewer', 'theme_enabled', true);
135
136
        if(!$this->supportsElemental()) {
137
            return;
138
        }
139
140
        if ($this->owner->hasMethod('ElementArea')) {
141
            $elements = $this->owner->ElementArea();
142
143
            if (!$elements->isInDB()) {
144
                $elements->write();
145
                $this->owner->ElementAreaID = $elements->ID;
146
            } else {
147
                // Copy widgets content to Content to enable search
148
                $searchableContent = array();
149
150
                Requirements::clear();
151
                foreach ($elements->Elements() as $element) {
152
                    if ($element->config()->exclude_from_content) {
153
                        continue;
154
                    }
155
156
                    $controller = $element->getController();
157
158
                    foreach ($elements->Items() as $element) {
159
                        $controller->init();
160
161
                        array_push($searchableContent, $controller->WidgetHolder());
162
                    }
163
                }
164
                Requirements::restore();
165
166
                $this->owner->Content = trim(implode(' ', $searchableContent));
167
            }
168
        }
169
170
171
        // set theme_enabled back to what it was
172
        Config::inst()->update('SSViewer', 'theme_enabled', $originalThemeEnabled);
173
174
        parent::onBeforeWrite();
175
    }
176
177
    /**
178
     * @return boolean
179
     */
180
    public function supportsElemental() {
181
        if (method_exists($this->owner, 'includeElemental')) {
182
            return $this->owner->includeElemental();
183
        }
184
185
        if (is_a($this->owner, 'RedirectorPage')) {
186
            return false;
187
        } else if ($ignored = Config::inst()->get('ElementPageExtension', 'ignored_classes')) {
188
            foreach ($ignored as $check) {
0 ignored issues
show
Bug introduced by
The expression $ignored of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
189
                if (is_a($this->owner, $check)) {
190
                    return false;
191
                }
192
            }
193
        }
194
195
        return true;
196
    }
197
198
    /**
199
     * If the page is duplicated, copy the widgets across too.
200
     *
201
     * Gets called twice from either direction, due to bad DataObject and SiteTree code, hence the weird if statement
202
     *
203
     * @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...
204
     */
205
    public function onAfterDuplicate($duplicatePage)
206
    {
207
        if ($this->owner->ID != 0 && $this->owner->ID < $duplicatePage->ID) {
208
            $originalWidgetArea = $this->owner->getComponent('ElementArea');
209
            $duplicateWidgetArea = $originalWidgetArea->duplicate(false);
210
            $duplicateWidgetArea->write();
211
            $duplicatePage->ElementAreaID = $duplicateWidgetArea->ID;
212
            $duplicatePage->write();
213
214 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...
215
                $duplicateWidget = $originalWidget->duplicate(true);
216
217
                // manually set the ParentID of each widget, so we don't get versioning issues
218
                DB::query(sprintf("UPDATE Widget SET ParentID = %d WHERE ID = %d", $duplicateWidgetArea->ID, $duplicateWidget->ID));
219
            }
220
        }
221
    }
222
223
    /**
224
     * If the page is duplicated across subsites, copy the widgets across too.
225
     *
226
     * @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...
227
     */
228
    public function onAfterDuplicateToSubsite($originalPage)
229
    {
230
        $originalWidgetArea = $originalPage->getComponent('ElementArea');
231
        $duplicateWidgetArea = $originalWidgetArea->duplicate(false);
232
        $duplicateWidgetArea->write();
233
        $this->owner->ElementAreaID = $duplicateWidgetArea->ID;
234
        $this->owner->write();
235
236 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...
237
            $duplicateWidget = $originalWidget->duplicate(true);
238
239
            // manually set the ParentID of each widget, so we don't get versioning issues
240
            DB::query(sprintf("UPDATE Widget SET ParentID = %d WHERE ID = %d", $duplicateWidgetArea->ID, $duplicateWidget->ID));
241
        }
242
    }
243
244
    /**
245
     * Publish
246
     */
247
    public function onAfterPublish()
248
    {
249
        if ($id = $this->owner->ElementAreaID) {
250
            $widgets = Versioned::get_by_stage('BaseElement', 'Stage', "ParentID = '$id'");
251
            $staged = array();
252
253
            foreach ($widgets as $widget) {
254
                $staged[] = $widget->ID;
255
256
                $widget->publish('Stage', 'Live');
257
            }
258
259
            // remove any elements that are on live but not in draft.
260
            $widgets = Versioned::get_by_stage('BaseElement', 'Live', "ParentID = '$id'");
261
262
            foreach ($widgets as $widget) {
263
                if (!in_array($widget->ID, $staged)) {
264
                    $widget->deleteFromStage('Live');
265
                }
266
            }
267
        }
268
    }
269
270
    /**
271
     * Roll back all changes if the parent page has a rollback event
272
     *
273
     * Only do rollback if it's the 'cancel draft changes' rollback, not a specific version
274
     * rollback.
275
     *
276
     * @param string $version
277
     * @return null
278
     */
279
    public function onBeforeRollback($version)
280
    {
281
        if ($version !== 'Live') {
282
            // we don't yet have a smart way of rolling back to a specific version
283
            return;
284
        }
285
        if ($id = $this->owner->ElementAreaID) {
286
            $widgets = Versioned::get_by_stage('BaseElement', 'Live', "ParentID = '$id'");
287
            $staged = array();
288
289
            foreach ($widgets as $widget) {
290
                $staged[] = $widget->ID;
291
292
                $widget->invokeWithExtensions('onBeforeRollback', $widget);
293
294
                $widget->publish("Live", "Stage", false);
295
296
                $widget->invokeWithExtensions('onAfterRollback', $widget);
297
            }
298
        }
299
    }
300
}
301