Passed
Pull Request — 4 (#930)
by Steve
03:44
created

updateCMSCompositeValidator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

120
                if ($inst->hasMethod('canCreateElement') && !$inst->/** @scrutinizer ignore-call */ canCreateElement()) {
Loading history...
121
                    continue;
122
                }
123
124
                $list[$availableClass] = $inst->getType();
125
            }
126
        }
127
128
        if ($config->get('sort_types_alphabetically') !== false) {
129
            asort($list);
130
        }
131
132
        if (isset($list[BaseElement::class])) {
133
            unset($list[BaseElement::class]);
134
        }
135
136
        $class = get_class($this->owner);
137
        $this->owner->invokeWithExtensions('updateAvailableTypesForClass', $class, $list);
138
139
        return $list;
140
    }
141
142
    /**
143
     * Returns an array of the relation names to ElementAreas. Ignores any
144
     * has_one fields named `Parent` as that would indicate that this is child
145
     * of an existing area
146
     *
147
     * @return array
148
     */
149
    public function getElementalRelations()
150
    {
151
        $hasOnes = $this->owner->hasOne();
152
153
        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...
154
            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...
155
        }
156
157
        $elementalAreaRelations = [];
158
159
        foreach ($hasOnes as $hasOneName => $hasOneClass) {
160
            if ($hasOneName === 'Parent' || $hasOneName === 'ParentID') {
161
                continue;
162
            }
163
164
            if ($hasOneClass == ElementalArea::class || is_subclass_of($hasOneClass, ElementalArea::class)) {
165
                $elementalAreaRelations[] = $hasOneName;
166
            }
167
        }
168
169
        return $elementalAreaRelations;
170
    }
171
172
    /**
173
     * Setup the CMS Fields
174
     *
175
     * @param FieldList
176
     */
177
    public function updateCMSFields(FieldList $fields)
178
    {
179
        if (!$this->supportsElemental()) {
180
            return;
181
        }
182
183
        // add an empty holder for content as some module explicitly use insert after content
184
        $globalReplace = !Config::inst()->get(self::class, 'keep_content_fields');
185
        $classOverride = Config::inst()->get(get_class($this->owner), 'elemental_keep_content_field');
186
        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...
187
            $fields->replaceField('Content', new LiteralField('Content', ''));
188
        }
189
        $elementalAreaRelations = $this->owner->getElementalRelations();
190
191
        foreach ($elementalAreaRelations as $eaRelationship) {
192
            $key = $eaRelationship . 'ID';
193
194
            // remove the scaffold dropdown
195
            $fields->removeByName($key);
196
197
            // remove the field, but don't add anything.
198
            if (!$this->owner->isInDb()) {
199
                continue;
200
            }
201
202
            // Example: $eaRelationship = 'ElementalArea';
203
            $area = $this->owner->$eaRelationship();
204
205
            $editor = ElementalAreaField::create($eaRelationship, $area, $this->getElementalTypes());
206
207
            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...
208
                $fields->addFieldToTab('Root.Main', $editor, 'Metadata');
209
            } else {
210
                $fields->addFieldToTab('Root.Main', $editor);
211
            }
212
        }
213
214
        return $fields;
215
    }
216
217
    /**
218
     * Make sure there is always an ElementalArea for adding Elements
219
     */
220
    public function onBeforeWrite()
221
    {
222
        parent::onBeforeWrite();
223
224
        if (!$this->supportsElemental()) {
225
            return;
226
        }
227
228
        $elementalAreaRelations = $this->owner->getElementalRelations();
229
230
        $this->ensureElementalAreasExist($elementalAreaRelations);
231
232
        $ownerClassName = get_class($this->owner);
233
234
        // Update the OwnerClassName on EA if the class has changed
235
        foreach ($elementalAreaRelations as $eaRelation) {
236
            $ea = $this->owner->$eaRelation();
237
            if ($ea->OwnerClassName !== $ownerClassName) {
238
                $ea->OwnerClassName = $ownerClassName;
239
                $ea->write();
240
            }
241
        }
242
243
        if (Config::inst()->get(self::class, 'clear_contentfield')) {
244
            $this->owner->Content = '';
245
        }
246
    }
247
248
    /**
249
     * @param CompositeValidator $compositeValidator
250
     */
251
    public function updateCMSCompositeValidator(CompositeValidator $compositeValidator): void
252
    {
253
        $compositeValidator->addValidator(ElementalAreasValidator::create());
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