Total Complexity | 65 |
Total Lines | 517 |
Duplicated Lines | 0 % |
Changes | 1 | ||
Bugs | 0 | Features | 1 |
Complex classes like ExternalContentItem often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use ExternalContentItem, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
29 | class ExternalContentItem extends DataObject |
||
30 | { |
||
31 | /** |
||
32 | * @var array |
||
33 | */ |
||
34 | private static $db = []; |
||
35 | |||
36 | /** |
||
37 | * @var string |
||
38 | */ |
||
39 | private static $table_name = 'ExternalContentItem'; |
||
40 | |||
41 | /** |
||
42 | * @var string The icon for cms tree |
||
43 | */ |
||
44 | private static $icon = null; |
||
45 | |||
46 | protected $ownerId; |
||
47 | |||
48 | /** |
||
49 | * The ID of this item in the remote system |
||
50 | * |
||
51 | * @var mixed |
||
52 | */ |
||
53 | protected $externalId; |
||
54 | |||
55 | public function setOwnerId($id) |
||
56 | { |
||
57 | $this->ownerId = $id; |
||
58 | } |
||
59 | |||
60 | /** |
||
61 | * The content source object that this item belongs to |
||
62 | * |
||
63 | * @var ExternalContentSource |
||
64 | */ |
||
65 | protected $source; |
||
66 | |||
67 | |||
68 | /** |
||
69 | * The child nodes of this item |
||
70 | * |
||
71 | * @var ArrayList |
||
72 | */ |
||
73 | private $children; |
||
74 | |||
75 | /** |
||
76 | * Create a new external content item. |
||
77 | * |
||
78 | * @param mixed $source |
||
79 | * The contentSource object this item was laoded through |
||
80 | * @param mixed $id |
||
81 | * The ID of the item in the remote system |
||
82 | * @param mixed $content |
||
83 | * A raw representation of the remote item. This allows for |
||
84 | * some systems loading up entire representations when you make |
||
85 | * a call to 'getChildren', for example. |
||
86 | */ |
||
87 | public function __construct($source = null, $id = null) |
||
88 | { |
||
89 | parent::__construct(); |
||
90 | if ($source) { |
||
91 | $this->source = $source; |
||
92 | $this->externalId = $id; |
||
93 | // if we are here, then we have been created in context of a parent, which also |
||
94 | // means there's a compound ID, so lets get that |
||
95 | $this->ID = $this->source->ID . ExternalContent::ID_SEPARATOR . $this->source->encodeId($id); |
||
96 | $this->ShowInMenus = $this->source->ShowContentInMenu; |
||
97 | $this->init(); |
||
98 | } |
||
99 | } |
||
100 | |||
101 | /** |
||
102 | * Return the ID of the item in its remote system |
||
103 | * |
||
104 | * @return string |
||
105 | */ |
||
106 | public function getExternalId() |
||
107 | { |
||
108 | return $this->externalId; |
||
109 | } |
||
110 | |||
111 | /** |
||
112 | * Get the type of this external object. |
||
113 | * |
||
114 | * Child classes must implement this as a method for certain functionality |
||
115 | * to know what the remote object is |
||
116 | * |
||
117 | * @return String |
||
118 | */ |
||
119 | public function getType() |
||
120 | { |
||
121 | throw new \Exception("Please implement " . get_class($this) . "::getType()"); |
||
122 | } |
||
123 | |||
124 | /** |
||
125 | * Initialise this object based on its source object |
||
126 | */ |
||
127 | protected function init() |
||
128 | { |
||
129 | } |
||
130 | |||
131 | /** |
||
132 | * Get the content source for this item |
||
133 | * |
||
134 | * @return ExternalContentSource |
||
135 | */ |
||
136 | public function getSource() |
||
137 | { |
||
138 | return $this->source; |
||
139 | } |
||
140 | |||
141 | /** |
||
142 | * Override to ensure exists handles things properly |
||
143 | * |
||
144 | * @return boolean |
||
145 | */ |
||
146 | public function exists() |
||
147 | { |
||
148 | return!is_null($this->ID); |
||
149 | } |
||
150 | |||
151 | /** |
||
152 | * Return a URL that simply links back to the externalcontentadmin |
||
153 | * class' 'view' action |
||
154 | * |
||
155 | * @param $action |
||
156 | * @return String |
||
157 | */ |
||
158 | public function Link($action = null) |
||
159 | { |
||
160 | $cur = Controller::curr(); |
||
161 | if ($cur instanceof ExternalContentPage_Controller) { |
||
162 | return $cur->data()->LinkFor($this, 'view'); |
||
163 | } |
||
164 | return ExternalContentPage_Controller::URL_STUB . '/view/' . $this->ID; |
||
165 | } |
||
166 | |||
167 | /** |
||
168 | * Return a URL that simply links back to the externalcontentadmin |
||
169 | * class' 'view' action |
||
170 | * |
||
171 | * @param $action |
||
172 | * @return String |
||
173 | */ |
||
174 | public function RelativeLink($action = null) |
||
175 | { |
||
176 | $cur = Controller::curr(); |
||
177 | if ($cur instanceof ExternalContentPage_Controller) { |
||
178 | return $cur->data()->LinkFor($this, 'view'); |
||
179 | } |
||
180 | return ExternalContentPage_Controller::URL_STUB . '/view/' . $this->ID; |
||
181 | } |
||
182 | |||
183 | /** |
||
184 | * Where this can be downloaded from |
||
185 | * |
||
186 | * @return string |
||
187 | */ |
||
188 | public function DownloadLink() |
||
189 | { |
||
190 | // get the base URL, prepend with the external content |
||
191 | // controller /download action and add this object's id |
||
192 | $cur = Controller::curr(); |
||
193 | if ($cur instanceof ExternalContentPage_Controller) { |
||
194 | return $cur->data()->LinkFor($this, 'download'); |
||
195 | } |
||
196 | return ExternalContentPage_Controller::URL_STUB . '/download/' . $this->ID; |
||
197 | } |
||
198 | |||
199 | /** |
||
200 | * Get the importer for this content item |
||
201 | * |
||
202 | * @return ExternalContentImporter |
||
203 | */ |
||
204 | public function getContentImporter($target=null) |
||
205 | { |
||
206 | return $this->source->getContentImporter($target); |
||
207 | } |
||
208 | |||
209 | /** |
||
210 | * Where can this content be imported to? |
||
211 | * |
||
212 | * @return array |
||
213 | */ |
||
214 | public function allowedImportTargets() |
||
215 | { |
||
216 | return $this->source->allowedImportTargets(); |
||
217 | } |
||
218 | |||
219 | /** |
||
220 | * An overrideable method to return the arbitrary 'content' of this |
||
221 | * node. Child classes should implement their own version |
||
222 | */ |
||
223 | public function Content() |
||
224 | { |
||
225 | } |
||
226 | |||
227 | /** |
||
228 | * Called to stream this content item (if it is streamable) |
||
229 | * |
||
230 | */ |
||
231 | public function streamContent() |
||
232 | { |
||
233 | throw new \Exception("This object cannot be streamed"); |
||
234 | } |
||
235 | |||
236 | /** |
||
237 | * Always return at least one as we never know til we load |
||
238 | * whether this item has children or not |
||
239 | * |
||
240 | * @return int |
||
241 | */ |
||
242 | public function numChildren() |
||
243 | { |
||
244 | return 1; |
||
245 | } |
||
246 | |||
247 | /** |
||
248 | * Overridden to load all children from a remote content |
||
249 | * source instead of this node directly |
||
250 | * |
||
251 | * @param boolean $showAll |
||
252 | * @return ArrayList |
||
253 | */ |
||
254 | public function stageChildren($showAll = false) |
||
255 | { |
||
256 | if ($this->Title != 'Content Root' && $this->source) { |
||
257 | $children = ArrayList::create(); |
||
258 | $item = ExternalContentItem::create($this->source, $this->Title . '1'); |
||
259 | $item->Title = $this->Title . '1'; |
||
260 | $item->MenuTitle = $item->Title; |
||
261 | |||
262 | $children->push($item); |
||
263 | return $children; |
||
264 | } |
||
265 | } |
||
266 | |||
267 | /** |
||
268 | * Handle a children call by retrieving from stageChildren |
||
269 | */ |
||
270 | public function Children() |
||
271 | { |
||
272 | if (!$this->children) { |
||
273 | $this->children = ArrayList::create(); |
||
274 | $kids = $this->stageChildren(); |
||
275 | if ($kids) { |
||
276 | foreach ($kids as $child) { |
||
277 | if ($child->canView()) { |
||
278 | $this->children->push($child); |
||
279 | } |
||
280 | } |
||
281 | } |
||
282 | } |
||
283 | return $this->children; |
||
284 | } |
||
285 | |||
286 | /** |
||
287 | * For now just show a field that says this can't be edited |
||
288 | * |
||
289 | * @see sapphire/core/model/DataObject#getCMSFields($params) |
||
290 | */ |
||
291 | public function getCMSFields() |
||
292 | { |
||
293 | $fields = parent::getCMSFields(); |
||
294 | |||
295 | $fields->removeByName('ParentID'); |
||
296 | if (count($this->remoteProperties)) { |
||
297 | $mapping = $this->editableFieldMapping(); |
||
298 | foreach ($this->remoteProperties as $name => $value) { |
||
299 | $field = null; |
||
300 | if (isset($mapping[$name])) { |
||
301 | $field = $mapping[$name]; |
||
302 | if (is_string($field)) { |
||
303 | $field = new $field($name, $this->fieldLabel($name), $value); |
||
304 | $fields->addFieldToTab('Root.Main', $field); |
||
305 | } |
||
306 | } elseif (!is_object($value) && !is_array($value)) { |
||
307 | $value = (string) $value; |
||
308 | $field = ReadonlyField::create($name, _t('ExternalContentItem.' . $name, $name), $value); |
||
309 | $fields->addFieldToTab('Root.Main', $field); |
||
310 | } elseif (is_object($value) || is_array($value)) { |
||
311 | foreach ($value as $childName => $childValue) { |
||
312 | if (is_object($childValue)) { |
||
313 | foreach ($childValue as $childChildName => $childChildValue) { |
||
314 | $childChildValue = is_object($childChildValue) || is_array($childChildValue) ? json_encode($childChildValue) : (string) $childChildValue; |
||
315 | $field = ReadonlyField::create("{$childName}{$childChildName}", "{$childName}: {$childChildName}", $childChildValue); |
||
316 | $fields->addFieldToTab('Root.Main', $field); |
||
317 | } |
||
318 | } else { |
||
319 | $childValue = is_object($childValue) || is_array($childValue) ? json_encode($childValue) : (string) $childValue; |
||
320 | $field = ReadonlyField::create("{$childName}{$childValue}", $name . ':' . $childName, $childValue); |
||
321 | $fields->addFieldToTab('Root.Main', $field); |
||
322 | } |
||
323 | } |
||
324 | } |
||
325 | } |
||
326 | } |
||
327 | |||
328 | return $fields; |
||
329 | } |
||
330 | |||
331 | /** |
||
332 | * Return a mapping of remote field name => field type |
||
333 | * |
||
334 | * Note that this also defines the list of fields on the remote object that can be edited by end users |
||
335 | * and is examined before a form save is triggered |
||
336 | * |
||
337 | */ |
||
338 | public function editableFieldMapping() |
||
339 | { |
||
340 | return array(); |
||
341 | } |
||
342 | |||
343 | /** |
||
344 | * Write back to the content source |
||
345 | */ |
||
346 | public function remoteWrite($member = null) |
||
347 | { |
||
348 | } |
||
349 | |||
350 | /** |
||
351 | * We flag external content as being editable so it's |
||
352 | * accessible in the backend, but the individual |
||
353 | * implementations will protect users from editing... for now |
||
354 | * |
||
355 | * TODO: Fix this up to use proper permission checks |
||
356 | * |
||
357 | * @see sapphire/core/model/DataObject#canEdit($member) |
||
358 | */ |
||
359 | public function canEdit($member = null) |
||
360 | { |
||
361 | return $this->source->canEdit(); |
||
362 | } |
||
363 | |||
364 | /** |
||
365 | * Is this item viewable? |
||
366 | * |
||
367 | * Just proxy to the content source for now. Child implementations can |
||
368 | * override if needbe |
||
369 | * |
||
370 | * @see sapphire/core/model/DataObject#canView($member) |
||
371 | */ |
||
372 | public function canView($member = null) |
||
373 | { |
||
374 | return $this->source->canView(); |
||
375 | } |
||
376 | |||
377 | /** |
||
378 | * Returns whether or not this source can be imported, defaulting to the |
||
379 | * linked source's permission. |
||
380 | * |
||
381 | * @return bool |
||
382 | */ |
||
383 | public function canImport() |
||
384 | { |
||
385 | return $this->source->canImport(); |
||
386 | } |
||
387 | |||
388 | /** |
||
389 | * The list of properties loaded from a remote data source |
||
390 | * |
||
391 | * @var array |
||
392 | */ |
||
393 | protected $remoteProperties; |
||
394 | |||
395 | /** |
||
396 | * Overriding the default behaviour to not worry about how it |
||
397 | * needs to work with the DB |
||
398 | * |
||
399 | * @see sapphire/core/ViewableData#__set($property, $value) |
||
400 | */ |
||
401 | public function __set($prop, $val) |
||
402 | { |
||
403 | $this->remoteProperties[$prop] = $val; |
||
404 | } |
||
405 | |||
406 | /** |
||
407 | * Return from the parent object if it's not in here... |
||
408 | * |
||
409 | * @see sapphire/core/ViewableData#__get($property) |
||
410 | */ |
||
411 | public function __get($prop) |
||
412 | { |
||
413 | if (isset($this->remoteProperties[$prop])) { |
||
414 | return $this->remoteProperties[$prop]; |
||
415 | } |
||
416 | |||
417 | $val = parent::__get($prop); |
||
418 | |||
419 | if (!$val) { |
||
420 | if ($this->source) { |
||
421 | // get it from there |
||
422 | return $this->source->$prop; |
||
423 | } |
||
424 | } |
||
425 | |||
426 | return $val; |
||
427 | } |
||
428 | |||
429 | /** |
||
430 | * Override to let remote objects figure out whether they have a |
||
431 | * field or not |
||
432 | * |
||
433 | * @see sapphire/core/model/DataObject#hasField($field) |
||
434 | */ |
||
435 | public function hasField($field) |
||
436 | { |
||
437 | return isset($this->remoteProperties[$field]); |
||
438 | } |
||
439 | |||
440 | /** |
||
441 | * Get all the remote properties |
||
442 | * |
||
443 | * @return array |
||
444 | */ |
||
445 | public function getRemoteProperties() |
||
446 | { |
||
447 | return $this->remoteProperties; |
||
448 | } |
||
449 | |||
450 | /** |
||
451 | * Perform a search query on this data source |
||
452 | * |
||
453 | * @param $filter A filter expression of some kind, in SQL format. |
||
454 | * @param $sort A sort expression, in SQL format. |
||
455 | * @param $join A join expression. May or may not be relevant. |
||
456 | * @param $limit A limit expression, either "(count)", or "(start), (count)" |
||
457 | */ |
||
458 | public function instance_get($filter = "", $sort = "", $join = "", $limit = "", $containerClass = "ArrayList") |
||
459 | { |
||
460 | } |
||
461 | |||
462 | /** |
||
463 | * Retrieve a single record from this data source |
||
464 | * |
||
465 | * @param $filter A filter expression of some kind, in SQL format. |
||
466 | * @param $sort A sort expression, in SQL format. |
||
467 | * @param $join A join expression. May or may not be relevant. |
||
468 | * @param $limit A limit expression, either "(count)", or "(start), (count)" |
||
469 | */ |
||
470 | public function instance_get_one($filter, $sort = "") |
||
471 | { |
||
472 | } |
||
473 | |||
474 | |||
475 | /** |
||
476 | * Write the current object back to the database. It should know whether this is a new object, in which case this would |
||
477 | * be an insert command, or if this is an existing object queried from the database, in which case thes would be |
||
478 | */ |
||
479 | public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false) |
||
480 | { |
||
481 | } |
||
482 | |||
483 | /** |
||
484 | * Remove this object from the database. Doesn't do anything if this object isn't in the database. |
||
485 | */ |
||
486 | public function delete() |
||
487 | { |
||
488 | } |
||
489 | |||
490 | /** |
||
491 | * Save content from a form into a field on this data object. |
||
492 | * Since the data comes straight from a form it can't be trusted and will need to be validated / escaped.' |
||
493 | */ |
||
494 | public function setCastedField($fieldName, $val) |
||
495 | { |
||
496 | $mapping = $this->editableFieldMapping(); |
||
497 | if (isset($mapping[$fieldName])) { |
||
498 | $this->$fieldName = $val; |
||
499 | } |
||
500 | } |
||
501 | |||
502 | /** |
||
503 | * Return the CSS classes to apply to this node in the CMS tree |
||
504 | * |
||
505 | * @return string |
||
506 | */ |
||
507 | public function CMSTreeClasses() |
||
508 | { |
||
509 | $classes = sprintf('class-%s', $this->class); |
||
510 | // Ensure that classes relating to whether there are further nodes to download are included |
||
511 | $classes .= $this->markingClasses(); |
||
512 | return $classes; |
||
513 | } |
||
514 | |||
515 | |||
516 | /** |
||
517 | * Return the CSS declarations to apply to nodes of this type in the CMS tree |
||
518 | * |
||
519 | * @return string |
||
520 | */ |
||
521 | public function CMSTreeCSS() |
||
522 | { |
||
523 | return null; |
||
524 | } |
||
525 | |||
526 | |||
527 | /** |
||
528 | * getTreeTitle will return two <span> html DOM elements, an empty <span> with |
||
529 | * the class 'jstree-pageicon' in front, following by a <span> wrapping around its |
||
530 | * MenutTitle |
||
531 | * |
||
532 | * @return string a html string ready to be directly used in a template |
||
533 | */ |
||
534 | public function getTreeTitle() |
||
535 | { |
||
536 | $title = $this->Title; |
||
537 | if (!$title) { |
||
538 | $title = $this->Name; |
||
539 | } |
||
540 | |||
541 | $treeTitle = sprintf( |
||
542 | "<span class=\"jstree-pageicon\"></span><span class=\"item\">%s</span>", |
||
543 | Convert::raw2xml(str_replace(array("\n","\r"), "", $title)) |
||
544 | ); |
||
545 | return $treeTitle; |
||
546 | } |
||
548 |