ElementalAreasExtension::updateCMSFields()   B
last analyzed

Complexity

Conditions 9
Paths 9

Size

Total Lines 38
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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

119
                if ($inst->hasMethod('canCreateElement') && !$inst->/** @scrutinizer ignore-call */ canCreateElement()) {
Loading history...
120
                    continue;
121
                }
122
123
                $list[$availableClass] = $inst->getType();
124
            }
125
        }
126
127
        if ($config->get('sort_types_alphabetically') !== false) {
128
            asort($list);
129
        }
130
131
        if (isset($list[BaseElement::class])) {
132
            unset($list[BaseElement::class]);
133
        }
134
135
        $class = get_class($this->owner);
136
        $this->owner->invokeWithExtensions('updateAvailableTypesForClass', $class, $list);
137
138
        return $list;
139
    }
140
141
    /**
142
     * Returns an array of the relation names to ElementAreas. Ignores any
143
     * has_one fields named `Parent` as that would indicate that this is child
144
     * of an existing area
145
     *
146
     * @return array
147
     */
148
    public function getElementalRelations()
149
    {
150
        $hasOnes = $this->owner->hasOne();
151
152
        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...
153
            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...
154
        }
155
156
        $elementalAreaRelations = [];
157
158
        foreach ($hasOnes as $hasOneName => $hasOneClass) {
159
            if ($hasOneName === 'Parent' || $hasOneName === 'ParentID') {
160
                continue;
161
            }
162
163
            if ($hasOneClass == ElementalArea::class || is_subclass_of($hasOneClass, ElementalArea::class)) {
164
                $elementalAreaRelations[] = $hasOneName;
165
            }
166
        }
167
168
        return $elementalAreaRelations;
169
    }
170
171
    /**
172
     * Setup the CMS Fields
173
     *
174
     * @param FieldList
175
     */
176
    public function updateCMSFields(FieldList $fields)
177
    {
178
        if (!$this->supportsElemental()) {
179
            return;
180
        }
181
182
        // add an empty holder for content as some module explicitly use insert after content
183
        $globalReplace = !Config::inst()->get(self::class, 'keep_content_fields');
184
        $classOverride = Config::inst()->get(get_class($this->owner), 'elemental_keep_content_field');
185
        if ($globalReplace && !$classOverride || $classOverride === false) {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($globalReplace && ! $cl...classOverride === false, Probably Intended Meaning: $globalReplace && (! $cl...lassOverride === false)
Loading history...
186
            $fields->replaceField('Content', new LiteralField('Content', ''));
187
        }
188
        $elementalAreaRelations = $this->owner->getElementalRelations();
189
190
        foreach ($elementalAreaRelations as $eaRelationship) {
191
            $key = $eaRelationship . 'ID';
192
193
            // remove the scaffold dropdown
194
            $fields->removeByName($key);
195
196
            // remove the field, but don't add anything.
197
            if (!$this->owner->isInDb()) {
198
                continue;
199
            }
200
201
            // Example: $eaRelationship = 'ElementalArea';
202
            $area = $this->owner->$eaRelationship();
203
204
            $editor = ElementalAreaField::create($eaRelationship, $area, $this->getElementalTypes());
205
206
            if ($this->owner instanceof SiteTree && $fields->findOrMakeTab('Root.Main')->fieldByName('Metadata')) {
0 ignored issues
show
Bug introduced by
Are you sure the usage of $fields->findOrMakeTab('...fieldByName('Metadata') targeting SilverStripe\Forms\CompositeField::fieldByName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
207
                $fields->addFieldToTab('Root.Main', $editor, 'Metadata');
208
            } else {
209
                $fields->addFieldToTab('Root.Main', $editor);
210
            }
211
        }
212
213
        return $fields;
214
    }
215
216
    /**
217
     * Make sure there is always an ElementalArea for adding Elements
218
     */
219
    public function onBeforeWrite()
220
    {
221
        parent::onBeforeWrite();
222
223
        if (!$this->supportsElemental()) {
224
            return;
225
        }
226
227
        $elementalAreaRelations = $this->owner->getElementalRelations();
228
229
        $this->ensureElementalAreasExist($elementalAreaRelations);
230
231
        $ownerClassName = get_class($this->owner);
232
233
        // Update the OwnerClassName on EA if the class has changed
234
        foreach ($elementalAreaRelations as $eaRelation) {
235
            $ea = $this->owner->$eaRelation();
236
            if ($ea->OwnerClassName !== $ownerClassName) {
237
                $ea->OwnerClassName = $ownerClassName;
238
                $ea->write();
239
            }
240
        }
241
242
        if (Config::inst()->get(self::class, 'clear_contentfield')) {
243
            $this->owner->Content = '';
244
        }
245
    }
246
247
    /**
248
     * @return boolean
249
     */
250
    public function supportsElemental()
251
    {
252
        if ($this->owner->hasMethod('includeElemental')) {
253
            $res = $this->owner->includeElemental();
254
255
            if ($res !== null) {
256
                return $res;
257
            }
258
        }
259
260
        if (is_a($this->owner, RedirectorPage::class) || is_a($this->owner, VirtualPage::class)) {
261
            return false;
262
        } elseif ($ignored = Config::inst()->get(ElementalPageExtension::class, 'ignored_classes')) {
263
            foreach ($ignored as $check) {
264
                if (is_a($this->owner, $check ?? '')) {
265
                    return false;
266
                }
267
            }
268
        }
269
270
        return true;
271
    }
272
273
    /**
274
     * Set all has_one relationships to an ElementalArea to a valid ID if they're unset
275
     *
276
     * @param array $elementalAreaRelations indexed array of relationship names that are to ElementalAreas
277
     * @return DataObject
278
     */
279
    public function ensureElementalAreasExist($elementalAreaRelations)
280
    {
281
        foreach ($elementalAreaRelations as $eaRelationship) {
282
            $areaID = $eaRelationship . 'ID';
283
284
            if (!$this->owner->$areaID) {
285
                $area = ElementalArea::create();
286
                $area->OwnerClassName = get_class($this->owner);
287
                $area->write();
288
                $this->owner->$areaID = $area->ID;
289
            }
290
        }
291
        return $this->owner;
292
    }
293
294
    /**
295
     * Extension hook {@see DataObject::requireDefaultRecords}
296
     *
297
     * @return void
298
     */
299
    public function requireDefaultRecords()
300
    {
301
        if (!$this->supportsElemental()) {
302
            return;
303
        }
304
305
        $this->owner->extend('onBeforeRequireDefaultElementalRecords');
306
307
        $ownerClass = get_class($this->owner);
308
        $elementalAreas = $this->owner->getElementalRelations();
309
        $schema = $this->owner->getSchema();
310
311
        // There is no inbuilt filter for null values
312
        $where = [];
313
        foreach ($elementalAreas as $areaName) {
314
            $queryDetails = $schema->sqlColumnForField($ownerClass, $areaName . 'ID');
315
            $where[] = $queryDetails . ' IS NULL OR ' . $queryDetails . ' = 0' ;
316
        }
317
318
        $records = $ownerClass::get()->where(implode(' OR ', $where));
319
        if ($ignored_classes = Config::inst()->get(ElementalPageExtension::class, 'ignored_classes')) {
320
            $records = $records->exclude('ClassName', $ignored_classes);
321
        }
322
323
        foreach ($records as $elementalObject) {
324
            if ($elementalObject->hasMethod('includeElemental')) {
325
                $res = $elementalObject->includeElemental();
326
                if ($res === false) {
327
                    continue;
328
                }
329
            }
330
331
            $needsPublishing = ViewableData::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\View\ViewableData::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

331
            $needsPublishing = ViewableData::has_extension(/** @scrutinizer ignore-type */ $elementalObject, Versioned::class)
Loading history...
332
                && $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

332
                && $elementalObject->/** @scrutinizer ignore-call */ isPublished();
Loading history...
333
334
            /** @var ElementalAreasExtension $elementalObject */
335
            $elementalObject->ensureElementalAreasExist($elementalAreas);
336
            $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

336
            $elementalObject->/** @scrutinizer ignore-call */ 
337
                              write();
Loading history...
337
            if ($needsPublishing) {
338
                $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

338
                $elementalObject->/** @scrutinizer ignore-call */ 
339
                                  publishRecursive();
Loading history...
339
            }
340
        }
341
342
        $this->owner->extend('onAfterRequireDefaultElementalRecords');
343
    }
344
}
345