Issues (124)

src/Extensions/ElementalAreasExtension.php (7 issues)

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
     * Get the available element types for this page type,
76
     *
77
     * Uses allowed_elements, stop_element_inheritance, disallowed_elements in
78
     * order to get to correct list.
79
     *
80
     * @return array
81
     */
82
    public function getElementalTypes()
83
    {
84
        $config = $this->owner->config();
85
86
        if (is_array($config->get('allowed_elements'))) {
87
            if ($config->get('stop_element_inheritance')) {
88
                $availableClasses = $config->get('allowed_elements', Config::UNINHERITED);
89
            } else {
90
                $availableClasses = $config->get('allowed_elements');
91
            }
92
        } else {
93
            $availableClasses = ClassInfo::subclassesFor(BaseElement::class);
94
        }
95
96
        if ($config->get('stop_element_inheritance')) {
97
            $disallowedElements = (array) $config->get('disallowed_elements', Config::UNINHERITED);
98
        } else {
99
            $disallowedElements = (array) $config->get('disallowed_elements');
100
        }
101
        $list = [];
102
103
        foreach ($availableClasses as $availableClass) {
104
            /** @var BaseElement $inst */
105
            $inst = singleton($availableClass);
106
107
            if (!in_array($availableClass, $disallowedElements) && $inst->canCreate()) {
108
                if ($inst->hasMethod('canCreateElement') && !$inst->canCreateElement()) {
0 ignored issues
show
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

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

294
            $needsPublishing = Extensible::has_extension(/** @scrutinizer ignore-type */ $elementalObject, Versioned::class)
Loading history...
295
                && $elementalObject->isPublished();
0 ignored issues
show
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

295
                && $elementalObject->/** @scrutinizer ignore-call */ isPublished();
Loading history...
296
297
            /** @var ElementalAreasExtension $elementalObject */
298
            $elementalObject->ensureElementalAreasExist($elementalAreas);
299
            $elementalObject->write();
0 ignored issues
show
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

299
            $elementalObject->/** @scrutinizer ignore-call */ 
300
                              write();
Loading history...
300
            if ($needsPublishing) {
301
                $elementalObject->publishRecursive();
0 ignored issues
show
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

301
                $elementalObject->/** @scrutinizer ignore-call */ 
302
                                  publishRecursive();
Loading history...
302
            }
303
        }
304
305
        $this->owner->extend('onAfterRequireDefaultElementalRecords');
306
    }
307
}
308