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\Forms\FieldList; |
||
14 | use SilverStripe\Forms\LiteralField; |
||
15 | use SilverStripe\ORM\DataExtension; |
||
16 | use SilverStripe\ORM\DataObject; |
||
17 | |||
18 | /** |
||
19 | * This extension handles most of the relationships between pages and element |
||
20 | * area, it doesn't add an ElementArea to the page however. Because of this, |
||
21 | * developers can add multiple {@link ElementArea} areas to to a page. |
||
22 | * |
||
23 | * If you want multiple ElementalAreas add them as has_ones, add this extensions |
||
24 | * and MAKE SURE you don't forget to add ElementAreas to $owns, otherwise they |
||
25 | * will never publish |
||
26 | * |
||
27 | * private static $has_one = array( |
||
28 | * 'ElementalArea1' => ElementalArea::class, |
||
29 | * 'ElementalArea2' => ElementalArea::class |
||
30 | * ); |
||
31 | * |
||
32 | * private static $owns = array( |
||
33 | * 'ElementalArea1', |
||
34 | * 'ElementalArea2' |
||
35 | * ); |
||
36 | * |
||
37 | * private static $cascade_duplicates = array( |
||
38 | * 'ElementalArea1', |
||
39 | * 'ElementalArea2' |
||
40 | * ); |
||
41 | * |
||
42 | * @package elemental |
||
43 | */ |
||
44 | class ElementalAreasExtension extends DataExtension |
||
45 | { |
||
46 | /** |
||
47 | * Classes to ignore adding elements to |
||
48 | * @config |
||
49 | * @var array $ignored_classes |
||
50 | */ |
||
51 | private static $ignored_classes = []; |
||
52 | |||
53 | /** |
||
54 | * On saving the element area, should Elemental reset the main website |
||
55 | * `$Content` field. |
||
56 | * |
||
57 | * @config |
||
58 | * @var boolean |
||
59 | */ |
||
60 | private static $clear_contentfield = false; |
||
61 | |||
62 | /** |
||
63 | * Whether to sort the elements alphabetically by their title |
||
64 | * |
||
65 | * @config |
||
66 | * @var boolean |
||
67 | */ |
||
68 | private static $sort_types_alphabetically = true; |
||
69 | |||
70 | /** |
||
71 | * Get the available element types for this page type, |
||
72 | * |
||
73 | * Uses allowed_elements, stop_element_inheritance, disallowed_elements in |
||
74 | * order to get to correct list. |
||
75 | * |
||
76 | * @return array |
||
77 | */ |
||
78 | public function getElementalTypes() |
||
79 | { |
||
80 | $config = $this->owner->config(); |
||
81 | |||
82 | if (is_array($config->get('allowed_elements'))) { |
||
83 | if ($config->get('stop_element_inheritance')) { |
||
84 | $availableClasses = $config->get('allowed_elements', Config::UNINHERITED); |
||
85 | } else { |
||
86 | $availableClasses = $config->get('allowed_elements'); |
||
87 | } |
||
88 | } else { |
||
89 | $availableClasses = ClassInfo::subclassesFor(BaseElement::class); |
||
90 | } |
||
91 | |||
92 | if ($config->get('stop_element_inheritance')) { |
||
93 | $disallowedElements = (array) $config->get('disallowed_elements', Config::UNINHERITED); |
||
94 | } else { |
||
95 | $disallowedElements = (array) $config->get('disallowed_elements'); |
||
96 | } |
||
97 | $list = []; |
||
98 | |||
99 | foreach ($availableClasses as $availableClass) { |
||
100 | /** @var BaseElement $inst */ |
||
101 | $inst = singleton($availableClass); |
||
102 | |||
103 | if (!in_array($availableClass, $disallowedElements) && $inst->canCreate()) { |
||
104 | if ($inst->hasMethod('canCreateElement') && !$inst->canCreateElement()) { |
||
105 | continue; |
||
106 | } |
||
107 | |||
108 | $list[$availableClass] = $inst->getType(); |
||
109 | } |
||
110 | } |
||
111 | |||
112 | if ($config->get('sort_types_alphabetically') !== false) { |
||
113 | asort($list); |
||
114 | } |
||
115 | |||
116 | if (isset($list[BaseElement::class])) { |
||
117 | unset($list[BaseElement::class]); |
||
118 | } |
||
119 | |||
120 | $class = get_class($this->owner); |
||
121 | $this->owner->invokeWithExtensions('updateAvailableTypesForClass', $class, $list); |
||
122 | |||
123 | return $list; |
||
124 | } |
||
125 | |||
126 | /** |
||
127 | * Returns an array of the relation names to ElementAreas. Ignores any |
||
128 | * has_one fields named `Parent` as that would indicate that this is child |
||
129 | * of an existing area |
||
130 | * |
||
131 | * @return array |
||
132 | */ |
||
133 | public function getElementalRelations() |
||
134 | { |
||
135 | $hasOnes = $this->owner->hasOne(); |
||
136 | |||
137 | if (!$hasOnes) { |
||
0 ignored issues
–
show
|
|||
138 | return false; |
||
139 | } |
||
140 | |||
141 | $elementalAreaRelations = []; |
||
142 | |||
143 | foreach ($hasOnes as $hasOneName => $hasOneClass) { |
||
144 | if ($hasOneName === 'Parent' || $hasOneName === 'ParentID') { |
||
145 | continue; |
||
146 | } |
||
147 | |||
148 | if ($hasOneClass == ElementalArea::class || is_subclass_of($hasOneClass, ElementalArea::class)) { |
||
149 | $elementalAreaRelations[] = $hasOneName; |
||
150 | } |
||
151 | } |
||
152 | |||
153 | return $elementalAreaRelations; |
||
154 | } |
||
155 | |||
156 | /** |
||
157 | * Setup the CMS Fields |
||
158 | * |
||
159 | * @param FieldList |
||
160 | */ |
||
161 | public function updateCMSFields(FieldList $fields) |
||
162 | { |
||
163 | if (!$this->supportsElemental()) { |
||
164 | return; |
||
165 | } |
||
166 | |||
167 | // add an empty holder for content as some module explicitly use insert |
||
168 | // after content. |
||
169 | $fields->replaceField('Content', new LiteralField('Content', '')); |
||
170 | $elementalAreaRelations = $this->owner->getElementalRelations(); |
||
171 | |||
172 | foreach ($elementalAreaRelations as $eaRelationship) { |
||
173 | $key = $eaRelationship . 'ID'; |
||
174 | |||
175 | // remove the scaffold dropdown |
||
176 | $fields->removeByName($key); |
||
177 | |||
178 | // remove the field, but don't add anything. |
||
179 | if (!$this->owner->isInDb()) { |
||
180 | continue; |
||
181 | } |
||
182 | |||
183 | // Example: $eaRelationship = 'ElementalArea'; |
||
184 | $area = $this->owner->$eaRelationship(); |
||
185 | |||
186 | $editor = ElementalAreaField::create($eaRelationship, $area, $this->getElementalTypes()); |
||
187 | |||
188 | if ($this->owner instanceof SiteTree && $fields->findOrMakeTab('Root.Main')->fieldByName('Metadata')) { |
||
189 | $fields->addFieldToTab('Root.Main', $editor, 'Metadata'); |
||
190 | } else { |
||
191 | $fields->addFieldToTab('Root.Main', $editor); |
||
192 | } |
||
193 | } |
||
194 | |||
195 | return $fields; |
||
196 | } |
||
197 | |||
198 | /** |
||
199 | * Make sure there is always an ElementalArea for adding Elements |
||
200 | */ |
||
201 | public function onBeforeWrite() |
||
202 | { |
||
203 | parent::onBeforeWrite(); |
||
204 | |||
205 | if (!$this->supportsElemental()) { |
||
206 | return; |
||
207 | } |
||
208 | |||
209 | $elementalAreaRelations = $this->owner->getElementalRelations(); |
||
210 | |||
211 | $this->ensureElementalAreasExist($elementalAreaRelations); |
||
212 | |||
213 | if (Config::inst()->get(self::class, 'clear_contentfield')) { |
||
214 | $this->owner->Content = ''; |
||
215 | } |
||
216 | } |
||
217 | |||
218 | /** |
||
219 | * @return boolean |
||
220 | */ |
||
221 | public function supportsElemental() |
||
222 | { |
||
223 | if ($this->owner->hasMethod('includeElemental')) { |
||
224 | $res = $this->owner->includeElemental(); |
||
225 | |||
226 | if ($res !== null) { |
||
227 | return $res; |
||
228 | } |
||
229 | } |
||
230 | |||
231 | if (is_a($this->owner, RedirectorPage::class) || is_a($this->owner, VirtualPage::class)) { |
||
232 | return false; |
||
233 | } elseif ($ignored = Config::inst()->get(ElementalPageExtension::class, 'ignored_classes')) { |
||
234 | foreach ($ignored as $check) { |
||
235 | if (is_a($this->owner, $check)) { |
||
236 | return false; |
||
237 | } |
||
238 | } |
||
239 | } |
||
240 | |||
241 | return true; |
||
242 | } |
||
243 | |||
244 | /** |
||
245 | * Set all has_one relationships to an ElementalArea to a valid ID if they're unset |
||
246 | * |
||
247 | * @param array $elementalAreaRelations indexed array of relationship names that are to ElementalAreas |
||
248 | * @return DataObject |
||
249 | */ |
||
250 | public function ensureElementalAreasExist($elementalAreaRelations) |
||
251 | { |
||
252 | foreach ($elementalAreaRelations as $eaRelationship) { |
||
253 | $areaID = $eaRelationship . 'ID'; |
||
254 | |||
255 | if (!$this->owner->$areaID) { |
||
256 | $area = ElementalArea::create(); |
||
257 | $area->OwnerClassName = get_class($this->owner); |
||
258 | $area->write(); |
||
259 | $this->owner->$areaID = $area->ID; |
||
260 | } |
||
261 | } |
||
262 | return $this->owner; |
||
263 | } |
||
264 | |||
265 | /** |
||
266 | * Extension hook {@see DataObject::requireDefaultRecords} |
||
267 | * |
||
268 | * @return void |
||
269 | */ |
||
270 | public function requireDefaultRecords() |
||
271 | { |
||
272 | if (!$this->supportsElemental()) { |
||
273 | return; |
||
274 | } |
||
275 | |||
276 | $ownerClass = get_class($this->owner); |
||
277 | $elementalAreas = $this->owner->getElementalRelations(); |
||
278 | $schema = $this->owner->getSchema(); |
||
279 | |||
280 | // There is no inbuilt filter for null values |
||
281 | $where = []; |
||
282 | foreach ($elementalAreas as $areaName) { |
||
283 | $queryDetails = $schema->sqlColumnForField($ownerClass, $areaName . 'ID'); |
||
284 | $where[] = $queryDetails . ' IS NULL OR ' . $queryDetails . ' = 0' ; |
||
285 | } |
||
286 | |||
287 | foreach ($ownerClass::get()->where(implode(' OR ', $where)) as $elementalObject) { |
||
288 | $elementalObject->ensureElementalAreasExist($elementalAreas)->write(); |
||
289 | } |
||
290 | } |
||
291 | } |
||
292 |
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.