Passed
Pull Request — master (#612)
by
unknown
03:17
created

ElementalAreasExtension::requireDefaultRecords()   A

Complexity

Conditions 6
Paths 17

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 16
nc 17
nop 0
dl 0
loc 28
rs 9.1111
c 0
b 0
f 0
1
<?php
2
3
namespace DNADesign\Elemental\Extensions;
4
5
use DNADesign\Elemental\Forms\ElementalAreaField;
6
use DNADesign\Elemental\Models\BaseElement;
7
use DNADesign\Elemental\Models\ElementalArea;
8
use SilverStripe\CMS\Model\RedirectorPage;
9
use SilverStripe\CMS\Model\SiteTree;
10
use SilverStripe\CMS\Model\VirtualPage;
11
use SilverStripe\Core\ClassInfo;
12
use SilverStripe\Core\Config\Config;
13
use SilverStripe\Forms\FieldList;
14
use SilverStripe\Forms\LiteralField;
15
use SilverStripe\ORM\DataExtension;
16
use SilverStripe\ORM\DataObject;
17
use SilverStripe\Subsites\Model\Subsite;
0 ignored issues
show
Bug introduced by
The type SilverStripe\Subsites\Model\Subsite 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...
18
19
/**
20
 * This extension handles most of the relationships between pages and element
21
 * area, it doesn't add an ElementArea to the page however. Because of this,
22
 * developers can add multiple {@link ElementArea} areas to to a page.
23
 *
24
 * If you want multiple ElementalAreas add them as has_ones, add this extensions
25
 * and MAKE SURE you don't forget to add ElementAreas to $owns, otherwise they
26
 * will never publish
27
 *
28
 * private static $has_one = array(
29
 *     'ElementalArea1' => ElementalArea::class,
30
 *     'ElementalArea2' => ElementalArea::class
31
 * );
32
 *
33
 * private static $owns = array(
34
 *     'ElementalArea1',
35
 *     'ElementalArea2'
36
 * );
37
 *
38
 * private static $cascade_duplicates = array(
39
 *     'ElementalArea1',
40
 *     'ElementalArea2'
41
 * );
42
 *
43
 * @package elemental
44
 */
45
class ElementalAreasExtension extends DataExtension
46
{
47
    /**
48
     * Classes to ignore adding elements to
49
     * @config
50
     * @var array $ignored_classes
51
     */
52
    private static $ignored_classes = [];
53
54
    /**
55
     * On saving the element area, should Elemental reset the main website
56
     * `$Content` field.
57
     *
58
     * @config
59
     * @var boolean
60
     */
61
    private static $clear_contentfield = false;
62
63
    /**
64
     * Whether to sort the elements alphabetically by their title
65
     *
66
     * @config
67
     * @var boolean
68
     */
69
    private static $sort_types_alphabetically = true;
70
71
    /**
72
     * Get the available element types for this page type,
73
     *
74
     * Uses allowed_elements, stop_element_inheritance, disallowed_elements in
75
     * order to get to correct list.
76
     *
77
     * @return array
78
     */
79
    public function getElementalTypes()
80
    {
81
        $config = $this->owner->config();
82
83
        if (is_array($config->get('allowed_elements'))) {
84
            if ($config->get('stop_element_inheritance')) {
85
                $availableClasses = $config->get('allowed_elements', Config::UNINHERITED);
86
            } else {
87
                $availableClasses = $config->get('allowed_elements');
88
            }
89
        } else {
90
            $availableClasses = ClassInfo::subclassesFor(BaseElement::class);
91
        }
92
93
        if ($config->get('stop_element_inheritance')) {
94
            $disallowedElements = (array) $config->get('disallowed_elements', Config::UNINHERITED);
95
        } else {
96
            $disallowedElements = (array) $config->get('disallowed_elements');
97
        }
98
        $list = [];
99
100
        foreach ($availableClasses as $availableClass) {
101
            /** @var BaseElement $inst */
102
            $inst = singleton($availableClass);
103
104
            if (!in_array($availableClass, $disallowedElements) && $inst->canCreate()) {
105
                if ($inst->hasMethod('canCreateElement') && !$inst->canCreateElement()) {
0 ignored issues
show
Bug introduced by
The method canCreateElement() 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

105
                if ($inst->hasMethod('canCreateElement') && !$inst->/** @scrutinizer ignore-call */ canCreateElement()) {
Loading history...
106
                    continue;
107
                }
108
109
                $list[$availableClass] = $inst->getType();
110
            }
111
        }
112
113
        if ($config->get('sort_types_alphabetically') !== false) {
114
            asort($list);
115
        }
116
117
        if (isset($list[BaseElement::class])) {
118
            unset($list[BaseElement::class]);
119
        }
120
121
        $this->owner->invokeWithExtensions('updateAvailableTypesForClass', $class, $list);
122
123
        return $list;
124
    }
125
126
    /**
127
     * Returns an array of the relation names to ElementAreas. Ignores any
128
     * has_one fields named `Parent` as that would indicate that this is child
129
     * of an existing area
130
     *
131
     * @return array
132
     */
133
    public function getElementalRelations()
134
    {
135
        $hasOnes = $this->owner->hasOne();
136
137
        if (!$hasOnes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $hasOnes 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...
138
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
139
        }
140
141
        $elementalAreaRelations = [];
142
143
        foreach ($hasOnes as $hasOneName => $hasOneClass) {
144
            if ($hasOneName === 'Parent' || $hasOneName === 'ParentID') {
145
                continue;
146
            }
147
148
            if ($hasOneClass == ElementalArea::class || is_subclass_of($hasOneClass, ElementalArea::class)) {
149
                $elementalAreaRelations[] = $hasOneName;
150
            }
151
        }
152
153
        return $elementalAreaRelations;
154
    }
155
156
    /**
157
     * Setup the CMS Fields
158
     *
159
     * @param FieldList
160
     */
161
    public function updateCMSFields(FieldList $fields)
162
    {
163
        if (!$this->supportsElemental()) {
164
            return;
165
        }
166
167
        // add an empty holder for content as some module explicitly use insert
168
        // after content.
169
        $fields->replaceField('Content', new LiteralField('Content', ''));
170
        $elementalAreaRelations = $this->owner->getElementalRelations();
171
172
        foreach ($elementalAreaRelations as $eaRelationship) {
173
            $key = $eaRelationship . 'ID';
174
175
            // remove the scaffold dropdown
176
            $fields->removeByName($key);
177
178
            // remove the field, but don't add anything.
179
            if (!$this->owner->isInDb()) {
180
                continue;
181
            }
182
183
            // Example: $eaRelationship = 'ElementalArea';
184
            $area = $this->owner->$eaRelationship();
185
186
            $editor = ElementalAreaField::create($eaRelationship, $area, $this->getElementalTypes());
187
188
            if ($this->owner instanceof SiteTree && $fields->findOrMakeTab('Root.Main')->fieldByName('Metadata')) {
189
                $fields->addFieldToTab('Root.Main', $editor, 'Metadata');
190
            } else {
191
                $fields->addFieldToTab('Root.Main', $editor);
192
            }
193
        }
194
195
        return $fields;
196
    }
197
198
    /**
199
     * Make sure there is always an ElementalArea for adding Elements
200
     */
201
    public function onBeforeWrite()
202
    {
203
        parent::onBeforeWrite();
204
205
        if (!$this->supportsElemental()) {
206
            return;
207
        }
208
209
        $elementalAreaRelations = $this->owner->getElementalRelations();
210
211
        $this->ensureElementalAreasExist($elementalAreaRelations);
212
213
        if (Config::inst()->get(self::class, 'clear_contentfield')) {
214
            $this->owner->Content = '';
215
        }
216
    }
217
218
    /**
219
     * @return boolean
220
     */
221
    public function supportsElemental()
222
    {
223
        if ($this->owner->hasMethod('includeElemental')) {
224
            $res = $this->owner->includeElemental();
225
226
            if ($res !== null) {
227
                return $res;
228
            }
229
        }
230
231
        if (is_a($this->owner, RedirectorPage::class) || is_a($this->owner, VirtualPage::class)) {
232
            return false;
233
        } elseif ($ignored = Config::inst()->get(ElementalPageExtension::class, 'ignored_classes')) {
234
            foreach ($ignored as $check) {
235
                if (is_a($this->owner, $check)) {
236
                    return false;
237
                }
238
            }
239
        }
240
241
        return true;
242
    }
243
244
    /**
245
     * Set all has_one relationships to an ElementalArea to a valid ID if they're unset
246
     *
247
     * @param array $elementalAreaRelations indexed array of relationship names that are to ElementalAreas
248
     * @return DataObject
249
     */
250
    public function ensureElementalAreasExist($elementalAreaRelations)
251
    {
252
        foreach ($elementalAreaRelations as $eaRelationship) {
253
            $areaID = $eaRelationship . 'ID';
254
255
            if (!$this->owner->$areaID) {
256
                $area = ElementalArea::create();
257
                $area->OwnerClassName = get_class($this->owner);
258
                $area->write();
259
                $this->owner->$areaID = $area->ID;
260
            }
261
        }
262
        return $this->owner;
263
    }
264
265
    /**
266
     * Extension hook {@see DataObject::requireDefaultRecords}
267
     *
268
     * @return void
269
     */
270
    public function requireDefaultRecords()
271
    {
272
        if (!$this->supportsElemental()) {
273
            return;
274
        }
275
276
        $ownerClass = get_class($this->owner);
277
        $elementalAreas = $this->owner->getElementalRelations();
278
        $schema = $this->owner->getSchema();
279
        $hasSubsites = class_exists('\SilverStripe\Subsites\Model\Subsite');
280
281
        if ($hasSubsites) {
282
            Subsite::$disable_subsite_filter = true;
283
        }
284
285
        // There is no inbuilt filter for null values
286
        $where = [];
287
        foreach ($elementalAreas as $areaName) {
288
            $queryDetails = $schema->sqlColumnForField($ownerClass, $areaName . 'ID');
289
            $where[] = $queryDetails . ' IS NULL OR ' . $queryDetails . ' = 0' ;
290
        }
291
292
        foreach ($ownerClass::get()->where(implode(' OR ', $where)) as $elementalObject) {
293
            $elementalObject->ensureElementalAreasExist($elementalAreas)->write();
294
        }
295
296
        if ($hasSubsites) {
297
            Subsite::$disable_subsite_filter = false;
298
        }
299
    }
300
}
301