Completed
Pull Request — master (#35)
by Robbie
02:10
created

ElementPageExtension::onAfterDuplicate()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 17
Code Lines 10

Duplication

Lines 6
Ratio 35.29 %

Importance

Changes 4
Bugs 1 Features 0
Metric Value
c 4
b 1
f 0
dl 6
loc 17
rs 9.2
cc 4
eloc 10
nc 3
nop 1
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');
0 ignored issues
show
Documentation introduced by
'ElementalArea' is of type string, but the function expects a array.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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
104
            if($this->owner->config()->get('sort_types_alphabetically') !== false) {
105
                $sorted = array();
106
107
                foreach ($list as $class) {
108
                    $inst = singleton($class);
109
110
                    if ($inst->canCreate()) {
111
                        $sorted[$class] = singleton($class)->i18n_singular_name();
112
                    }
113
                }
114
115
                $list = $sorted;
116
                asort($list);
117
            }
118
        } else {
119
            $classes = ClassInfo::subclassesFor('BaseElement');
120
            $list = array();
121
            unset($classes['BaseElement']);
122
123
            $disallowedElements = (array) $this->owner->config()->get('disallowed_elements');
124
125
            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...
126
                $inst = singleton($class);
127
128
                if (!in_array($class, $disallowedElements) && $inst->canCreate()) {
129
                    $list[$class] = singleton($class)->i18n_singular_name();
130
                }
131
            }
132
133
            asort($list);
134
        }
135
136
        if (method_exists($this->owner, 'sortElementalOptions')) {
137
            $this->owner->sortElementalOptions($list);
138
        }
139
140
        return $list;
141
    }
142
143
    /**
144
     * Make sure there is always a WidgetArea sidebar for adding widgets
145
     *
146
     */
147
    public function onBeforeWrite()
148
    {
149
        // enable theme in case elements are being rendered with templates stored in theme folder
150
        $originalThemeEnabled = Config::inst()->get('SSViewer', 'theme_enabled');
151
        Config::inst()->update('SSViewer', 'theme_enabled', true);
152
153
        if(!$this->supportsElemental()) {
154
            return;
155
        }
156
157
        if ($this->owner->hasMethod('ElementArea')) {
158
            $elements = $this->owner->ElementArea();
159
160
            if (!$elements->isInDB()) {
161
                $elements->write();
162
                $this->owner->ElementAreaID = $elements->ID;
163
            } else {
164
                // Copy widgets content to Content to enable search
165
                $searchableContent = array();
166
167
                Requirements::clear();
168
                foreach ($elements->Elements() as $element) {
169
                    if ($element->config()->exclude_from_content) {
170
                        continue;
171
                    }
172
173
                    $controller = $element->getController();
174
175
                    foreach ($elements->Items() as $element) {
176
                        $controller->init();
177
178
                        array_push($searchableContent, $controller->WidgetHolder());
179
                    }
180
                }
181
                Requirements::restore();
182
183
                $this->owner->Content = trim(implode(' ', $searchableContent));
184
            }
185
        }
186
187
188
        // set theme_enabled back to what it was
189
        Config::inst()->update('SSViewer', 'theme_enabled', $originalThemeEnabled);
190
191
        parent::onBeforeWrite();
192
    }
193
194
    /**
195
     * @return boolean
196
     */
197
    public function supportsElemental() {
198
        if (method_exists($this->owner, 'includeElemental')) {
199
            return $this->owner->includeElemental();
200
        }
201
202
        if (is_a($this->owner, 'RedirectorPage')) {
203
            return false;
204
        } else if ($ignored = Config::inst()->get('ElementPageExtension', 'ignored_classes')) {
205
            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...
206
                if (is_a($this->owner, $check)) {
207
                    return false;
208
                }
209
            }
210
        }
211
212
        return true;
213
    }
214
215
    /**
216
     * If the page is duplicated, copy the widgets across too.
217
     *
218
     * Gets called twice from either direction, due to bad DataObject and SiteTree code, hence the weird if statement
219
     *
220
     * @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...
221
     */
222
    public function onAfterDuplicate($duplicatePage)
223
    {
224
        if ($this->owner->ID != 0 && $this->owner->ID < $duplicatePage->ID) {
225
            $originalWidgetArea = $this->owner->getComponent('ElementArea');
226
            $duplicateWidgetArea = $originalWidgetArea->duplicate(false);
227
            $duplicateWidgetArea->write();
228
            $duplicatePage->ElementAreaID = $duplicateWidgetArea->ID;
229
            $duplicatePage->write();
230
231 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...
232
                $duplicateWidget = $originalWidget->duplicate(true);
233
234
                // manually set the ParentID of each widget, so we don't get versioning issues
235
                DB::query(sprintf("UPDATE Widget SET ParentID = %d WHERE ID = %d", $duplicateWidgetArea->ID, $duplicateWidget->ID));
236
            }
237
        }
238
    }
239
240
    /**
241
     * If the page is duplicated across subsites, copy the widgets across too.
242
     *
243
     * @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...
244
     */
245
    public function onAfterDuplicateToSubsite($originalPage)
246
    {
247
        $originalWidgetArea = $originalPage->getComponent('ElementArea');
248
        $duplicateWidgetArea = $originalWidgetArea->duplicate(false);
249
        $duplicateWidgetArea->write();
250
        $this->owner->ElementAreaID = $duplicateWidgetArea->ID;
251
        $this->owner->write();
252
253 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...
254
            $duplicateWidget = $originalWidget->duplicate(true);
255
256
            // manually set the ParentID of each widget, so we don't get versioning issues
257
            DB::query(sprintf("UPDATE Widget SET ParentID = %d WHERE ID = %d", $duplicateWidgetArea->ID, $duplicateWidget->ID));
258
        }
259
    }
260
261
    /**
262
     * Publish
263
     */
264
    public function onAfterPublish()
265
    {
266
        if ($id = $this->owner->ElementAreaID) {
267
            $widgets = Versioned::get_by_stage('BaseElement', 'Stage', "ParentID = '$id'");
268
            $staged = array();
269
270
            foreach ($widgets as $widget) {
271
                $staged[] = $widget->ID;
272
273
                $widget->publish('Stage', 'Live');
274
            }
275
276
            // remove any elements that are on live but not in draft.
277
            $widgets = Versioned::get_by_stage('BaseElement', 'Live', "ParentID = '$id'");
278
279
            foreach ($widgets as $widget) {
280
                if (!in_array($widget->ID, $staged)) {
281
                    $widget->deleteFromStage('Live');
282
                }
283
            }
284
        }
285
    }
286
287
    /**
288
     * Roll back all changes if the parent page has a rollback event
289
     *
290
     * Only do rollback if it's the 'cancel draft changes' rollback, not a specific version
291
     * rollback.
292
     *
293
     * @param string $version
294
     * @return null
295
     */
296
    public function onBeforeRollback($version)
297
    {
298
        if ($version !== 'Live') {
299
            // we don't yet have a smart way of rolling back to a specific version
300
            return;
301
        }
302
        if ($id = $this->owner->ElementAreaID) {
303
            $widgets = Versioned::get_by_stage('BaseElement', 'Live', "ParentID = '$id'");
304
            $staged = array();
305
306
            foreach ($widgets as $widget) {
307
                $staged[] = $widget->ID;
308
309
                $widget->invokeWithExtensions('onBeforeRollback', $widget);
310
311
                $widget->publish("Live", "Stage", false);
312
313
                $widget->invokeWithExtensions('onAfterRollback', $widget);
314
            }
315
        }
316
    }
317
}
318