Passed
Pull Request — 4 (#822)
by Steve
04:53
created

ElementalAreasExtension::supportsElemental()   B

Complexity

Conditions 8
Paths 9

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 11
c 0
b 0
f 0
nc 9
nop 0
dl 0
loc 21
rs 8.4444
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
     * Applies globally, across all page types; unless a page type overrides this with its own config setting of
77
     * `elemental_keep_content_field`
78
     *
79
     * @var boolean
80
     * @config
81
     */
82
    private static $keep_content_fields = false;
83
84
    /**
85
     * Get the available element types for this page type,
86
     *
87
     * Uses allowed_elements, stop_element_inheritance, disallowed_elements in
88
     * order to get to correct list.
89
     *
90
     * @return array
91
     */
92
    public function getElementalTypes()
93
    {
94
        $config = $this->owner->config();
95
96
        if (is_array($config->get('allowed_elements'))) {
97
            if ($config->get('stop_element_inheritance')) {
98
                $availableClasses = $config->get('allowed_elements', Config::UNINHERITED);
99
            } else {
100
                $availableClasses = $config->get('allowed_elements');
101
            }
102
        } else {
103
            $availableClasses = ClassInfo::subclassesFor(BaseElement::class);
104
        }
105
106
        if ($config->get('stop_element_inheritance')) {
107
            $disallowedElements = (array) $config->get('disallowed_elements', Config::UNINHERITED);
108
        } else {
109
            $disallowedElements = (array) $config->get('disallowed_elements');
110
        }
111
        $list = [];
112
113
        foreach ($availableClasses as $availableClass) {
114
            /** @var BaseElement $inst */
115
            $inst = singleton($availableClass);
116
117
            if (!in_array($availableClass, $disallowedElements) && $inst->canCreate()) {
118
                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

118
                if ($inst->hasMethod('canCreateElement') && !$inst->/** @scrutinizer ignore-call */ canCreateElement()) {
Loading history...
119
                    continue;
120
                }
121
122
                $list[$availableClass] = $inst->getType();
123
            }
124
        }
125
126
        if ($config->get('sort_types_alphabetically') !== false) {
127
            asort($list);
128
        }
129
130
        if (isset($list[BaseElement::class])) {
131
            unset($list[BaseElement::class]);
132
        }
133
134
        $class = get_class($this->owner);
135
        $this->owner->invokeWithExtensions('updateAvailableTypesForClass', $class, $list);
136
137
        return $list;
138
    }
139
140
    /**
141
     * Returns an array of the relation names to ElementAreas. Ignores any
142
     * has_one fields named `Parent` as that would indicate that this is child
143
     * of an existing area
144
     *
145
     * @return array
146
     */
147
    public function getElementalRelations()
148
    {
149
        $hasOnes = $this->owner->hasOne();
150
151
        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...
152
            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...
153
        }
154
155
        $elementalAreaRelations = [];
156
157
        foreach ($hasOnes as $hasOneName => $hasOneClass) {
158
            if ($hasOneName === 'Parent' || $hasOneName === 'ParentID') {
159
                continue;
160
            }
161
162
            if ($hasOneClass == ElementalArea::class || is_subclass_of($hasOneClass, ElementalArea::class)) {
163
                $elementalAreaRelations[] = $hasOneName;
164
            }
165
        }
166
167
        return $elementalAreaRelations;
168
    }
169
170
    /**
171
     * Setup the CMS Fields
172
     *
173
     * @param FieldList
174
     */
175
    public function updateCMSFields(FieldList $fields)
176
    {
177
        if (!$this->supportsElemental()) {
178
            return;
179
        }
180
181
        // add an empty holder for content as some module explicitly use insert after content
182
        $globalReplace = !Config::inst()->get(self::class, 'keep_content_fields');
183
        $classOverride = Config::inst()->get(get_class($this->owner), 'elemental_keep_content_field');
184
        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...
185
            $fields->replaceField('Content', new LiteralField('Content', ''));
186
        }
187
        $elementalAreaRelations = $this->owner->getElementalRelations();
188
189
        foreach ($elementalAreaRelations as $eaRelationship) {
190
            $key = $eaRelationship . 'ID';
191
192
            // remove the scaffold dropdown
193
            $fields->removeByName($key);
194
195
            // remove the field, but don't add anything.
196
            if (!$this->owner->isInDb()) {
197
                continue;
198
            }
199
200
            // Example: $eaRelationship = 'ElementalArea';
201
            $area = $this->owner->$eaRelationship();
202
203
            $editor = ElementalAreaField::create($eaRelationship, $area, $this->getElementalTypes());
204
205
            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...
206
                $fields->addFieldToTab('Root.Main', $editor, 'Metadata');
207
            } else {
208
                $fields->addFieldToTab('Root.Main', $editor);
209
            }
210
        }
211
212
        return $fields;
213
    }
214
215
    /**
216
     * Make sure there is always an ElementalArea for adding Elements
217
     */
218
    public function onBeforeWrite()
219
    {
220
        parent::onBeforeWrite();
221
222
        if (!$this->supportsElemental()) {
223
            return;
224
        }
225
226
        $elementalAreaRelations = $this->owner->getElementalRelations();
227
228
        $this->ensureElementalAreasExist($elementalAreaRelations);
229
230
        $ownerClassName = get_class($this->owner);
231
232
        foreach ($elementalAreaRelations as $eaRelation) {
233
            // Update the OwnerClassName on EA if the class has changed
234
            /** @var ElementalArea $ea */
235
            $ea = $this->owner->$eaRelation();
236
            if ($ea->OwnerClassName !== $ownerClassName) {
237
                $ea->OwnerClassName = $ownerClassName;
238
                $ea->write();
239
            }
240
241
            // Owner page will have set its own HasBrokenLink / HasBrokenFile in SiteTreeLinkTracking / FileLinkTracking
242
            // on any $page->Content BEFORE we get to this point
243
            if (count(array_filter($ea->Elements()->column('HasBrokenLink')))) {
244
                $this->owner->HasBrokenLink = true;
245
            }
246
            if (count(array_filter($ea->Elements()->column('HasBrokenFile')))) {
247
                $this->owner->HasBrokenFile = true;
248
            }
249
        }
250
251
        if (Config::inst()->get(self::class, 'clear_contentfield')) {
252
            $this->owner->Content = '';
253
        }
254
    }
255
256
    /**
257
     * @return boolean
258
     */
259
    public function supportsElemental()
260
    {
261
        if ($this->owner->hasMethod('includeElemental')) {
262
            $res = $this->owner->includeElemental();
263
264
            if ($res !== null) {
265
                return $res;
266
            }
267
        }
268
269
        if (is_a($this->owner, RedirectorPage::class) || is_a($this->owner, VirtualPage::class)) {
270
            return false;
271
        } elseif ($ignored = Config::inst()->get(ElementalPageExtension::class, 'ignored_classes')) {
272
            foreach ($ignored as $check) {
273
                if (is_a($this->owner, $check)) {
274
                    return false;
275
                }
276
            }
277
        }
278
279
        return true;
280
    }
281
282
    /**
283
     * Set all has_one relationships to an ElementalArea to a valid ID if they're unset
284
     *
285
     * @param array $elementalAreaRelations indexed array of relationship names that are to ElementalAreas
286
     * @return DataObject
287
     */
288
    public function ensureElementalAreasExist($elementalAreaRelations)
289
    {
290
        foreach ($elementalAreaRelations as $eaRelationship) {
291
            $areaID = $eaRelationship . 'ID';
292
293
            if (!$this->owner->$areaID) {
294
                $area = ElementalArea::create();
295
                $area->OwnerClassName = get_class($this->owner);
296
                $area->write();
297
                $this->owner->$areaID = $area->ID;
298
            }
299
        }
300
        return $this->owner;
301
    }
302
303
    /**
304
     * Extension hook {@see DataObject::requireDefaultRecords}
305
     *
306
     * @return void
307
     */
308
    public function requireDefaultRecords()
309
    {
310
        if (!$this->supportsElemental()) {
311
            return;
312
        }
313
314
        $this->owner->extend('onBeforeRequireDefaultElementalRecords');
315
316
        $ownerClass = get_class($this->owner);
317
        $elementalAreas = $this->owner->getElementalRelations();
318
        $schema = $this->owner->getSchema();
319
320
        // There is no inbuilt filter for null values
321
        $where = [];
322
        foreach ($elementalAreas as $areaName) {
323
            $queryDetails = $schema->sqlColumnForField($ownerClass, $areaName . 'ID');
324
            $where[] = $queryDetails . ' IS NULL OR ' . $queryDetails . ' = 0' ;
325
        }
326
327
        $records = $ownerClass::get()->where(implode(' OR ', $where));
328
        if ($ignored_classes = Config::inst()->get(ElementalPageExtension::class, 'ignored_classes')) {
329
            $records = $records->exclude('ClassName', $ignored_classes);
330
        }
331
332
        foreach ($records as $elementalObject) {
333
            if ($elementalObject->hasMethod('includeElemental')) {
334
                $res = $elementalObject->includeElemental();
335
                if ($res === false) {
336
                    continue;
337
                }
338
            }
339
340
            $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

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

341
                && $elementalObject->/** @scrutinizer ignore-call */ isPublished();
Loading history...
342
343
            /** @var ElementalAreasExtension $elementalObject */
344
            $elementalObject->ensureElementalAreasExist($elementalAreas);
345
            $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

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

347
                $elementalObject->/** @scrutinizer ignore-call */ 
348
                                  publishRecursive();
Loading history...
348
            }
349
        }
350
351
        $this->owner->extend('onAfterRequireDefaultElementalRecords');
352
    }
353
}
354