Test Failed
Push — master ( 357bf5...71cdad )
by Russell
03:53
created

ExternalContentItem   F

Complexity

Total Complexity 65

Size/Duplication

Total Lines 517
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
wmc 65
eloc 107
c 1
b 0
f 1
dl 0
loc 517
rs 3.2

How to fix   Complexity   

Complex Class

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
2
3
use SilverStripe\ORM\DataObject;
4
use SilverStripe\Control\Controller;
5
use SilverStripe\Core\Config\Configurable;
6
use SilverStripe\ORM\ArrayList;
7
use SilverStripe\Core\Convert;
8
use SilverStripe\Core\Extensible;
9
use SilverStripe\Core\Injector\Injectable;
10
use SilverStripe\Forms\ReadonlyField;
11
12
/**
13
 * Parent class for all ExternalContentItems.
14
 *
15
 * On construction, an ExternalContentItem subclass must load data from the
16
 * remote repository through the appropriate API layer as returned by getRemoteRepository()
17
 * from the external content source. It is then up to the content item to
18
 * store that data in a way that can be used by the rest of SilverStripe.
19
 *
20
 * For now, the ExternalContentItem provides the remoteProperties map for
21
 * storing things, with __get and __set magic methods for retrieving values.
22
 * Some implementations may choose to store the data in a separate object (for
23
 * example, the AlfrescoContentItem implementation simply stores things in its
24
 * contained CMS object and maps back and forward from that).
25
 *
26
 * @author Marcus Nyeholt <[email protected]>
27
 * @license BSD License http://silverstripe.org/bsd-license
28
 */
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
    }
547
}
548