Completed
Pull Request — master (#54)
by
unknown
08:45
created

ElementPageExtension::onAfterDelete()   C

Complexity

Conditions 7
Paths 11

Size

Total Lines 35
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 35
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 23
nc 11
nop 0
1
<?php
2
3
use \Heyday\VersionedDataObjects\VersionedDataObjectDetailsForm;
4
5
/**
6
 * @package elemental
7
 */
8
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...
9
{
10
11
    /**
12
     * @config
13
     *
14
     * @var string $elements_title Title of the element in the CMS.
15
     */
16
    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...
17
18
    /**
19
     * @config
20
     *
21
     * @var boolean $disable_element_publish_button Disable publish / unpublish buttons in GridFieldDetailForm.
22
     */
23
    private static $disable_element_publish_button = false;
0 ignored issues
show
Unused Code introduced by
The property $disable_element_publish_button 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...
24
25
    /**
26
     * @config
27
     *
28
     * @var array $ignored_classes Classes to ignore adding elements too.
29
     */
30
    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...
31
32
    /**
33
     * @config
34
     *
35
     * @var boolean
36
     */
37
    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...
38
39
    /**
40
     * @config
41
     *
42
     * @var boolean
43
     */
44
    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...
45
46
    /**
47
     * @var array $db
48
     */
49
    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...
50
51
    /**
52
     * @var array $has_one
53
     */
54
    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...
55
        'ElementArea' => 'ElementalArea'
56
    );
57
58
    /**
59
     * Setup the CMS Fields
60
     *
61
     * @param FieldList
62
     */
63
    public function updateCMSFields(FieldList $fields)
64
    {
65
        if(!$this->supportsElemental()) {
66
            return;
67
        }
68
69
        // add an empty holder for content as some module explicitly use insert
70
        // after content.
71
        $fields->replaceField('Content', new LiteralField('Content', ''));
72
73
        $adder = new ElementalGridFieldAddNewMultiClass();
74
75
        $list = $this->getAvailableTypes();
76
        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...
77
            $adder->setClasses($list);
78
        }
79
80
        $area = $this->owner->ElementArea();
81
        if ($this->owner->exists() && (!$area->exists() || !$area->isInDB())) {
82
            $area->write();
83
84
            $this->owner->ElementAreaID = $area->ID;
85
            $this->owner->write();
86
        }
87
88
        $gridField = GridField::create('ElementArea',
89
            Config::inst()->get("ElementPageExtension", 'elements_title'),
90
            $area->AllElements(),
91
            GridFieldConfig_RelationEditor::create()
92
                ->removeComponentsByType('GridFieldAddNewButton')
93
                ->removeComponentsByType('GridFieldSortableHeader')
94
                ->removeComponentsByType('GridFieldDeleteAction')
95
                ->removeComponentsByType('GridFieldAddExistingAutocompleter')
96
                ->addComponent($autocomplete = new ElementalGridFieldAddExistingAutocompleter())
97
                ->addComponent(new ElementalGridFieldDeleteAction())
98
                ->addComponent(new GridFieldTitleHeader())
99
                ->addComponent($adder)
100
                ->addComponent(new GridFieldSortableRows('Sort'))
101
        );
102
103
        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...
104
            $autocomplete->setSearchList(BaseElement::get()->filter('ClassName', array_keys($list)));
105
        }
106
107
        $autocomplete->setResultsFormat('($ID) $Title');
108
        $autocomplete->setSearchFields(array('ID', 'Title'));
109
110
        $config = $gridField->getConfig();
111
        $paginator = $config->getComponentByType('GridFieldPaginator');
112
        $paginator->setItemsPerPage(100);
113
114
        if (!$this->owner->config()->disable_element_publish_button) {
115
            $config->removeComponentsByType('GridFieldDetailForm');
116
            $config->addComponent($obj = new VersionedDataObjectDetailsForm());
117
        }
118
119
        if ($this->owner instanceof SiteTree && $fields->findOrMakeTab('Root.Main')->fieldByName('Metadata')) {
120
            $fields->addFieldToTab('Root.Main', $gridField, 'Metadata');
121
        } else {
122
            $fields->addFieldToTab('Root.Main', $gridField);
123
        }
124
125
        return $fields;
126
    }
127
128
    /**
129
     * @return array
130
     */
131
    public function getAvailableTypes() {
132
        if (is_array($this->owner->config()->get('allowed_elements'))) {
133
            $list = $this->owner->config()->get('allowed_elements');
134
135 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...
136
                $sorted = array();
137
138
                foreach ($list as $class) {
139
                    $inst = singleton($class);
140
141
                    if ($inst->canCreate()) {
142
                        $sorted[$class] = singleton($class)->i18n_singular_name();
143
                    }
144
                }
145
146
                $list = $sorted;
147
                asort($list);
148
            }
149 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...
150
            $classes = ClassInfo::subclassesFor('BaseElement');
151
            $list = array();
152
            unset($classes['BaseElement']);
153
154
            $disallowedElements = (array) $this->owner->config()->get('disallowed_elements');
155
156
            if (!in_array('ElementVirtualLinked', $disallowedElements)) {
157
                array_push($disallowedElements, 'ElementVirtualLinked');
158
            }
159
160
            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...
161
                $inst = singleton($class);
162
163
                if (!in_array($class, $disallowedElements) && $inst->canCreate()) {
164
                    $list[$class] = singleton($class)->i18n_singular_name();
165
                }
166
            }
167
168
            asort($list);
169
        }
170
171
        if (method_exists($this->owner, 'sortElementalOptions')) {
172
            $this->owner->sortElementalOptions($list);
173
        }
174
175
        return $list;
176
    }
177
178
    /**
179
     * Make sure there is always a WidgetArea sidebar for adding widgets
180
     *
181
     */
182
    public function onBeforeWrite()
183
    {
184
        if(!$this->supportsElemental()) {
185
            return;
186
        }
187
188
        // enable theme in case elements are being rendered with templates stored in theme folder
189
        $originalThemeEnabled = Config::inst()->get('SSViewer', 'theme_enabled');
190
        Config::inst()->update('SSViewer', 'theme_enabled', true);
191
192
193
        if ($this->owner->hasMethod('ElementArea') && Config::inst()->get(__CLASS__, 'copy_element_content_to_contentfield')) {
194
            $elements = $this->owner->ElementArea();
195
196
            if (!$elements->isInDB()) {
197
                $elements->write();
198
                $this->owner->ElementAreaID = $elements->ID;
199
            } else {
200
                // Copy widgets content to Content to enable search
201
                $searchableContent = array();
202
203
                Requirements::clear();
204
205
                foreach ($elements->Elements() as $element) {
206
                    if ($element->config()->exclude_from_content) {
207
                        continue;
208
                    }
209
210
                    $controller = $element->getController();
211
                    $controller->init();
212
213
                    array_push($searchableContent, $controller->WidgetHolder());
214
                }
215
216
                Requirements::restore();
217
218
                $this->owner->Content = trim(implode(' ', $searchableContent));
219
            }
220
        } else {
221
            if(Config::inst()->get(__CLASS__, 'clear_contentfield')) {
222
                $this->owner->Content = '';
223
            }
224
        }
225
226
227
        // set theme_enabled back to what it was
228
        Config::inst()->update('SSViewer', 'theme_enabled', $originalThemeEnabled);
229
230
        parent::onBeforeWrite();
231
    }
232
233
    /**
234
     * Ensure that if there are elements that belong to this page
235
     * and are virtualised (Virtual Element links to them), that we move the
236
     * original element to replace one of the virtual elements
237
     * But only if it's a delete not an unpublish
238
     */
239
    public function onAfterDelete() {
240
        if(Versioned::get_reading_mode() == 'Stage.Stage') {
241
            $area = $this->owner->ElementArea();
242
            foreach ($area->Widgets() as $element) {
243
                $firstVirtual = false;
244
                if ($element->getPublishedVirtualLinkedElements()->Count() > 0) {
245
                    // choose the first one
246
                    $firstVirtual = $element->getPublishedVirtualLinkedElements()->First();
247
                    $wasPublished = true;
248
                } else if ($element->getVirtualLinkedElements()->Count() > 0) {
249
                    // choose the first one
250
                    $firstVirtual = $element->getVirtualLinkedElements()->First();
251
                    $wasPublished = false;
252
                }
253
                if ($firstVirtual) {
254
                    $origParentID = $element->ParentID;
255
                    $origSort = $element->Sort;
256
257
                    // change element to first's values
258
                    $element->ParentID = $firstVirtual->ParentID;
259
                    $element->Sort = $firstVirtual->Sort;
260
261
                    $firstVirtual->ParentID = $origParentID;
262
                    $firstVirtual->Sort = $origSort;
263
                    // write
264
                    $element->write();
265
                    $firstVirtual->write();
266
                    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...
267
                        $element->doPublish();
268
                        $firstVirtual->doPublish();
269
                    }
270
                }
271
            }
272
        }
273
    }
274
275
    /**
276
     * @return boolean
277
     */
278
    public function supportsElemental() {
279
        if ($this->owner->hasMethod('includeElemental')) {
280
            $res = $this->owner->includeElemental();
281
            if ($res !== null) {
282
                return $res;
283
            }
284
        }
285
286
        if (is_a($this->owner, 'RedirectorPage')) {
287
            return false;
288
        } else if ($ignored = Config::inst()->get('ElementPageExtension', 'ignored_classes')) {
289
            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...
290
                if (is_a($this->owner, $check)) {
291
                    return false;
292
                }
293
            }
294
        }
295
296
        return true;
297
    }
298
299
    /**
300
     * If the page is duplicated, copy the widgets across too.
301
     *
302
     * Gets called twice from either direction, due to bad DataObject and SiteTree code, hence the weird if statement
303
     *
304
     * @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...
305
     */
306
    public function onAfterDuplicate($duplicatePage)
307
    {
308
        if ($this->owner->ID != 0 && $this->owner->ID < $duplicatePage->ID) {
309
            $originalWidgetArea = $this->owner->getComponent('ElementArea');
310
            $duplicateWidgetArea = $originalWidgetArea->duplicate(false);
311
            $duplicateWidgetArea->write();
312
            $duplicatePage->ElementAreaID = $duplicateWidgetArea->ID;
313
            $duplicatePage->write();
314
315 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...
316
                $duplicateWidget = $originalWidget->duplicate(true);
317
318
                // manually set the ParentID of each widget, so we don't get versioning issues
319
                DB::query(sprintf("UPDATE Widget SET ParentID = %d WHERE ID = %d", $duplicateWidgetArea->ID, $duplicateWidget->ID));
320
            }
321
        }
322
    }
323
324
    /**
325
     * If the page is duplicated across subsites, copy the widgets across too.
326
     *
327
     * @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...
328
     */
329
    public function onAfterDuplicateToSubsite($originalPage)
330
    {
331
        $originalWidgetArea = $originalPage->getComponent('ElementArea');
332
        $duplicateWidgetArea = $originalWidgetArea->duplicate(false);
333
        $duplicateWidgetArea->write();
334
        $this->owner->ElementAreaID = $duplicateWidgetArea->ID;
335
        $this->owner->write();
336
337 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...
338
            $duplicateWidget = $originalWidget->duplicate(true);
339
340
            // manually set the ParentID of each widget, so we don't get versioning issues
341
            DB::query(sprintf("UPDATE Widget SET ParentID = %d WHERE ID = %d", $duplicateWidgetArea->ID, $duplicateWidget->ID));
342
        }
343
    }
344
345
    /**
346
     * Publish
347
     */
348
    public function onAfterPublish()
349
    {
350
        if ($id = $this->owner->ElementAreaID) {
351
            $widgets = Versioned::get_by_stage('BaseElement', 'Stage', "ParentID = '$id'");
352
            $staged = array();
353
354
            foreach ($widgets as $widget) {
355
                $staged[] = $widget->ID;
356
357
                $widget->publish('Stage', 'Live');
358
            }
359
360
            // remove any elements that are on live but not in draft.
361
            $widgets = Versioned::get_by_stage('BaseElement', 'Live', "ParentID = '$id'");
362
363
            foreach ($widgets as $widget) {
364
                if (!in_array($widget->ID, $staged)) {
365
                    $widget->deleteFromStage('Live');
366
                }
367
            }
368
        }
369
    }
370
371
    /**
372
     * Roll back all changes if the parent page has a rollback event
373
     *
374
     * Only do rollback if it's the 'cancel draft changes' rollback, not a specific version
375
     * rollback.
376
     *
377
     * @param string $version
378
     * @return null
379
     */
380
    public function onBeforeRollback($version)
381
    {
382
        if ($version !== 'Live') {
383
            // we don't yet have a smart way of rolling back to a specific version
384
            return;
385
        }
386
        if ($id = $this->owner->ElementAreaID) {
387
            $widgets = Versioned::get_by_stage('BaseElement', 'Live', "ParentID = '$id'");
388
            $staged = array();
389
390
            foreach ($widgets as $widget) {
391
                $staged[] = $widget->ID;
392
393
                $widget->invokeWithExtensions('onBeforeRollback', $widget);
394
395
                $widget->publish("Live", "Stage", false);
396
397
                $widget->invokeWithExtensions('onAfterRollback', $widget);
398
            }
399
        }
400
    }
401
}
402