Passed
Pull Request — master (#732)
by
unknown
03:44
created

ElementalAreasExtension::onBeforeWrite()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

304
            $needsPublishing = Extensible::has_extension(/** @scrutinizer ignore-type */ $elementalObject, Versioned::class)
Loading history...
305
                && $elementalObject->isPublished();
0 ignored issues
show
Bug introduced by
The method isPublished() does not exist on DNADesign\Elemental\Exte...ElementalAreasExtension. 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

305
                && $elementalObject->/** @scrutinizer ignore-call */ isPublished();
Loading history...
306
307
            /** @var ElementalAreasExtension $elementalObject */
308
            $elementalObject->ensureElementalAreasExist($elementalAreas);
309
            $elementalObject->write();
0 ignored issues
show
Bug introduced by
The method write() does not exist on DNADesign\Elemental\Exte...ElementalAreasExtension. 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

309
            $elementalObject->/** @scrutinizer ignore-call */ 
310
                              write();
Loading history...
310
            if ($needsPublishing) {
311
                $elementalObject->publishRecursive();
0 ignored issues
show
Bug introduced by
The method publishRecursive() does not exist on DNADesign\Elemental\Exte...ElementalAreasExtension. 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

311
                $elementalObject->/** @scrutinizer ignore-call */ 
312
                                  publishRecursive();
Loading history...
312
            }
313
        }
314
315
        $this->owner->extend('onAfterRequireDefaultElementalRecords');
316
    }
317
}
318