Passed
Push — master ( ecccb9...b91912 )
by Robbie
08:46
created

ElementalAreasExtension::requireDefaultRecords()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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

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