DataFormatter   B
last analyzed

Complexity

Total Complexity 50

Size/Duplication

Total Lines 442
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 94
c 2
b 0
f 0
dl 0
loc 442
rs 8.4
wmc 50

23 Methods

Rating   Name   Duplication   Size   Complexity  
A sanitiseClassName() 0 3 1
A setCustomRelations() 0 4 1
A getRemoveFields() 0 3 1
A getCustomFields() 0 3 1
A getCustomRelations() 0 3 1
A getCustomAddFields() 0 3 1
A getOutputContentType() 0 3 1
A getTotalSize() 0 3 1
A convertStringToArray() 0 3 1
A getRealFieldName() 0 4 1
A for_mimetypes() 0 9 3
A for_extension() 0 13 4
A getMappedKey() 0 10 3
A getApiMapping() 0 7 3
A getRealFields() 0 11 4
A for_extensions() 0 9 3
A for_mimetype() 0 13 4
A getFieldAlias() 0 5 1
A setRemoveFields() 0 4 1
B getFieldsForObj() 0 37 11
A setTotalSize() 0 4 1
A setCustomFields() 0 4 1
A setCustomAddFields() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like DataFormatter 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 DataFormatter, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\RestfulServer;
4
5
use SilverStripe\Core\ClassInfo;
6
use SilverStripe\Core\Config\Config;
7
use SilverStripe\Core\Config\Configurable;
8
use SilverStripe\ORM\DataObject;
9
use SilverStripe\ORM\DataObjectInterface;
10
use SilverStripe\ORM\SS_List;
11
12
/**
13
 * A DataFormatter object handles transformation of data from SilverStripe model objects to a particular output
14
 * format, and vice versa.  This is most commonly used in developing RESTful APIs.
15
 */
16
abstract class DataFormatter
17
{
18
19
    use Configurable;
20
21
    /**
22
     * Set priority from 0-100.
23
     * If multiple formatters for the same extension exist,
24
     * we select the one with highest priority.
25
     *
26
     * @var int
27
     */
28
    private static $priority = 50;
0 ignored issues
show
introduced by
The private property $priority is not used, and could be removed.
Loading history...
29
30
    /**
31
     * Follow relations for the {@link DataObject} instances
32
     * ($has_one, $has_many, $many_many).
33
     * Set to "0" to disable relation output.
34
     *
35
     * @todo Support more than one nesting level
36
     *
37
     * @var int
38
     */
39
    public $relationDepth = 1;
40
41
    /**
42
     * Allows overriding of the fields which are rendered for the
43
     * processed dataobjects. By default, this includes all
44
     * fields in {@link DataObject::inheritedDatabaseFields()}.
45
     *
46
     * @var array
47
     */
48
    protected $customFields = null;
49
50
    /**
51
     * Allows addition of fields
52
     * (e.g. custom getters on a DataObject)
53
     *
54
     * @var array
55
     */
56
    protected $customAddFields = null;
57
58
    /**
59
     * Allows to limit or add relations.
60
     * Only use in combination with {@link $relationDepth}.
61
     * By default, all relations will be shown.
62
     *
63
     * @var array
64
     */
65
    protected $customRelations = null;
66
67
    /**
68
     * Fields which should be expicitly excluded from the export.
69
     * Comes in handy for field-level permissions.
70
     * Will overrule both {@link $customAddFields} and {@link $customFields}
71
     *
72
     * @var array
73
     */
74
    protected $removeFields = null;
75
76
    /**
77
     * Specifies the mimetype in which all strings
78
     * returned from the convert*() methods should be used,
79
     * e.g. "text/xml".
80
     *
81
     * @var string
82
     */
83
    protected $outputContentType = null;
84
85
    /**
86
     * Used to set totalSize properties on the output
87
     * of {@link convertDataObjectSet()}, shows the
88
     * total number of records without the "limit" and "offset"
89
     * GET parameters. Useful to implement pagination.
90
     *
91
     * @var int
92
     */
93
    protected $totalSize;
94
95
    /**
96
     * Backslashes in fully qualified class names (e.g. NameSpaced\ClassName)
97
     * kills both requests (i.e. URIs) and XML (invalid character in a tag name)
98
     * So we'll replace them with a hyphen (-), as it's also unambiguious
99
     * in both cases (invalid in a php class name, and safe in an xml tag name)
100
     *
101
     * @param string $classname
102
     * @return string 'escaped' class name
103
     */
104
    protected function sanitiseClassName($className)
105
    {
106
        return str_replace('\\', '-', $className);
107
    }
108
109
    /**
110
     * Get a DataFormatter object suitable for handling the given file extension.
111
     *
112
     * @param string $extension
113
     * @return DataFormatter
114
     */
115
    public static function for_extension($extension)
116
    {
117
        $classes = ClassInfo::subclassesFor(DataFormatter::class);
118
        array_shift($classes);
119
        $sortedClasses = [];
120
        foreach ($classes as $class) {
121
            $sortedClasses[$class] = Config::inst()->get($class, 'priority');
122
        }
123
        arsort($sortedClasses);
124
        foreach ($sortedClasses as $className => $priority) {
125
            $formatter = new $className();
126
            if (in_array($extension, $formatter->supportedExtensions())) {
127
                return $formatter;
128
            }
129
        }
130
    }
131
132
    /**
133
     * Get formatter for the first matching extension.
134
     *
135
     * @param array $extensions
136
     * @return DataFormatter
137
     */
138
    public static function for_extensions($extensions)
139
    {
140
        foreach ($extensions as $extension) {
141
            if ($formatter = self::for_extension($extension)) {
142
                return $formatter;
143
            }
144
        }
145
146
        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 SilverStripe\RestfulServer\DataFormatter.
Loading history...
147
    }
148
149
    /**
150
     * Get a DataFormatter object suitable for handling the given mimetype.
151
     *
152
     * @param string $mimeType
153
     * @return DataFormatter
154
     */
155
    public static function for_mimetype($mimeType)
156
    {
157
        $classes = ClassInfo::subclassesFor(DataFormatter::class);
158
        array_shift($classes);
159
        $sortedClasses = [];
160
        foreach ($classes as $class) {
161
            $sortedClasses[$class] = Config::inst()->get($class, 'priority');
162
        }
163
        arsort($sortedClasses);
164
        foreach ($sortedClasses as $className => $priority) {
165
            $formatter = new $className();
166
            if (in_array($mimeType, $formatter->supportedMimeTypes())) {
167
                return $formatter;
168
            }
169
        }
170
    }
171
172
    /**
173
     * Get formatter for the first matching mimetype.
174
     * Useful for HTTP Accept headers which can contain
175
     * multiple comma-separated mimetypes.
176
     *
177
     * @param array $mimetypes
178
     * @return DataFormatter
179
     */
180
    public static function for_mimetypes($mimetypes)
181
    {
182
        foreach ($mimetypes as $mimetype) {
183
            if ($formatter = self::for_mimetype($mimetype)) {
184
                return $formatter;
185
            }
186
        }
187
188
        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 SilverStripe\RestfulServer\DataFormatter.
Loading history...
189
    }
190
191
    /**
192
     * @param array $fields
193
     * @return $this
194
     */
195
    public function setCustomFields($fields)
196
    {
197
        $this->customFields = $fields;
198
        return $this;
199
    }
200
201
    /**
202
     * @return array
203
     */
204
    public function getCustomFields()
205
    {
206
        return $this->customFields;
207
    }
208
209
    /**
210
     * @param array $fields
211
     * @return $this
212
     */
213
    public function setCustomAddFields($fields)
214
    {
215
        $this->customAddFields = $fields;
216
        return $this;
217
    }
218
219
    /**
220
     * @param array $relations
221
     * @return $this
222
     */
223
    public function setCustomRelations($relations)
224
    {
225
        $this->customRelations = $relations;
226
        return $this;
227
    }
228
229
    /**
230
     * @return array
231
     */
232
    public function getCustomRelations()
233
    {
234
        return $this->customRelations;
235
    }
236
237
    /**
238
     * @return array
239
     */
240
    public function getCustomAddFields()
241
    {
242
        return $this->customAddFields;
243
    }
244
245
    /**
246
     * @param array $fields
247
     * @return $this
248
     */
249
    public function setRemoveFields($fields)
250
    {
251
        $this->removeFields = $fields;
252
        return $this;
253
    }
254
255
    /**
256
     * @return array
257
     */
258
    public function getRemoveFields()
259
    {
260
        return $this->removeFields;
261
    }
262
263
    /**
264
     * @return string
265
     */
266
    public function getOutputContentType()
267
    {
268
        return $this->outputContentType;
269
    }
270
271
    /**
272
     * @param int $size
273
     * @return $this
274
     */
275
    public function setTotalSize($size)
276
    {
277
        $this->totalSize = (int)$size;
278
        return $this;
279
    }
280
281
    /**
282
     * @return int
283
     */
284
    public function getTotalSize()
285
    {
286
        return $this->totalSize;
287
    }
288
289
    /**
290
     * Returns all fields on the object which should be shown
291
     * in the output. Can be customised through {@link self::setCustomFields()}.
292
     *
293
     * @todo Allow for custom getters on the processed object (currently filtered through inheritedDatabaseFields)
294
     * @todo Field level permission checks
295
     *
296
     * @param DataObject $obj
297
     * @return array
298
     */
299
    protected function getFieldsForObj($obj)
300
    {
301
        $dbFields = [];
302
303
        // if custom fields are specified, only select these
304
        if (is_array($this->customFields)) {
0 ignored issues
show
introduced by
The condition is_array($this->customFields) is always true.
Loading history...
305
            foreach ($this->customFields as $fieldName) {
306
                // @todo Possible security risk by making methods accessible - implement field-level security
307
                if (($obj->hasField($fieldName) && !is_object($obj->getField($fieldName)))
308
                    || $obj->hasMethod("get{$fieldName}")
309
                ) {
310
                    $dbFields[$fieldName] = $fieldName;
311
                }
312
            }
313
        } else {
314
            // by default, all database fields are selected
315
            $dbFields = DataObject::getSchema()->fieldSpecs(get_class($obj));
316
            // $dbFields = $obj->inheritedDatabaseFields();
317
        }
318
319
        if (is_array($this->customAddFields)) {
0 ignored issues
show
introduced by
The condition is_array($this->customAddFields) is always true.
Loading history...
320
            foreach ($this->customAddFields as $fieldName) {
321
                // @todo Possible security risk by making methods accessible - implement field-level security
322
                if ($obj->hasField($fieldName) || $obj->hasMethod("get{$fieldName}")) {
323
                    $dbFields[$fieldName] = $fieldName;
324
                }
325
            }
326
        }
327
328
        // add default required fields
329
        $dbFields = array_merge($dbFields, ['ID' => 'Int']);
330
331
        if (is_array($this->removeFields)) {
0 ignored issues
show
introduced by
The condition is_array($this->removeFields) is always true.
Loading history...
332
            $dbFields = array_diff_key($dbFields, array_combine($this->removeFields, $this->removeFields));
0 ignored issues
show
Bug introduced by
It seems like array_combine($this->rem...s, $this->removeFields) can also be of type false; however, parameter $array2 of array_diff_key() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

332
            $dbFields = array_diff_key($dbFields, /** @scrutinizer ignore-type */ array_combine($this->removeFields, $this->removeFields));
Loading history...
333
        }
334
335
        return $dbFields;
336
    }
337
338
    /**
339
     * Return an array of the extensions that this data formatter supports
340
     */
341
    abstract public function supportedExtensions();
342
343
    abstract public function supportedMimeTypes();
344
345
    /**
346
     * Convert a single data object to this format. Return a string.
347
     *
348
     * @param DataObjectInterface $do
349
     * @return mixed
350
     */
351
    abstract public function convertDataObject(DataObjectInterface $do);
352
353
    /**
354
     * Convert a data object set to this format. Return a string.
355
     *
356
     * @param SS_List $set
357
     * @return string
358
     */
359
    abstract public function convertDataObjectSet(SS_List $set);
360
361
    /**
362
     * Convert an array to this format. Return a string.
363
     *
364
     * @param $array
365
     * @return string
366
     */
367
    abstract public function convertArray($array);
368
369
    /**
370
     * @param string $strData HTTP Payload as string
371
     */
372
    public function convertStringToArray($strData)
0 ignored issues
show
Unused Code introduced by
The parameter $strData is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

372
    public function convertStringToArray(/** @scrutinizer ignore-unused */ $strData)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
373
    {
374
        user_error('DataFormatter::convertStringToArray not implemented on subclass', E_USER_ERROR);
375
    }
376
377
    /**
378
     * Convert an array of aliased field names to their Dataobject field name
379
     *
380
     * @param string $className
381
     * @param string[] $fields
382
     * @return string[]
383
     */
384
    public function getRealFields($className, $fields)
385
    {
386
        $apiMapping = $this->getApiMapping($className);
387
        if (is_array($apiMapping) && is_array($fields)) {
0 ignored issues
show
introduced by
The condition is_array($fields) is always true.
Loading history...
388
            $mappedFields = [];
389
            foreach ($fields as $field) {
390
                $mappedFields[] = $this->getMappedKey($apiMapping, $field);
391
            }
392
            return $mappedFields;
393
        }
394
        return $fields;
395
    }
396
397
    /**
398
     * Get the DataObject field name from its alias
399
     *
400
     * @param string $className
401
     * @param string $field
402
     * @return string
403
     */
404
    public function getRealFieldName($className, $field)
405
    {
406
        $apiMapping = $this->getApiMapping($className);
407
        return $this->getMappedKey($apiMapping, $field);
408
    }
409
410
    /**
411
     * Get a DataObject Field's Alias
412
     * defaults to the fieldname
413
     *
414
     * @param string $className
415
     * @param string $field
416
     * @return string
417
     */
418
    public function getFieldAlias($className, $field)
419
    {
420
        $apiMapping = $this->getApiMapping($className);
421
        $apiMapping = array_flip($apiMapping);
422
        return $this->getMappedKey($apiMapping, $field);
423
    }
424
425
    /**
426
     * Get the 'api_field_mapping' config value for a class
427
     * or return an empty array
428
     *
429
     * @param string $className
430
     * @return string[]|array
431
     */
432
    protected function getApiMapping($className)
433
    {
434
        $apiMapping = Config::inst()->get($className, 'api_field_mapping');
435
        if ($apiMapping && is_array($apiMapping)) {
436
            return $apiMapping;
437
        }
438
        return [];
439
    }
440
441
    /**
442
     * Helper function to get mapped field names
443
     *
444
     * @param array $map
445
     * @param string $key
446
     * @return string
447
     */
448
    protected function getMappedKey($map, $key)
449
    {
450
        if (is_array($map)) {
0 ignored issues
show
introduced by
The condition is_array($map) is always true.
Loading history...
451
            if (array_key_exists($key, $map)) {
452
                return $map[$key];
453
            } else {
454
                return $key;
455
            }
456
        }
457
        return $key;
458
    }
459
}
460