Completed
Push — new-committers ( 29cb6f...bcba16 )
by Sam
12:18 queued 33s
created

DataFormatter::setCustomFields()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
nop 1
1
<?php
2
/**
3
 * A DataFormatter object handles transformation of data from SilverStripe model objects to a particular output
4
 * format, and vice versa.  This is most commonly used in developing RESTful APIs.
5
 *
6
 * @package framework
7
 * @subpackage formatters
8
 */
9
abstract class DataFormatter extends Object {
10
11
	/**
12
	 * Set priority from 0-100.
13
	 * If multiple formatters for the same extension exist,
14
	 * we select the one with highest priority.
15
	 *
16
	 * @var int
17
	 */
18
	private static $priority = 50;
19
20
	/**
21
	 * Follow relations for the {@link DataObject} instances
22
	 * ($has_one, $has_many, $many_many).
23
	 * Set to "0" to disable relation output.
24
	 *
25
	 * @todo Support more than one nesting level
26
	 *
27
	 * @var int
28
	 */
29
	public $relationDepth = 1;
30
31
	/**
32
	 * Allows overriding of the fields which are rendered for the
33
	 * processed dataobjects. By default, this includes all
34
	 * fields in {@link DataObject::inheritedDatabaseFields()}.
35
	 *
36
	 * @var array
37
	 */
38
	protected $customFields = null;
39
40
	/**
41
	 * Allows addition of fields
42
	 * (e.g. custom getters on a DataObject)
43
	 *
44
	 * @var array
45
	 */
46
	protected $customAddFields = null;
47
48
	/**
49
	 * Allows to limit or add relations.
50
	 * Only use in combination with {@link $relationDepth}.
51
	 * By default, all relations will be shown.
52
	 *
53
	 * @var array
54
	 */
55
	protected $customRelations = null;
56
57
	/**
58
	 * Fields which should be expicitly excluded from the export.
59
	 * Comes in handy for field-level permissions.
60
	 * Will overrule both {@link $customAddFields} and {@link $customFields}
61
	 *
62
	 * @var array
63
	 */
64
	protected $removeFields = null;
65
66
	/**
67
	 * Specifies the mimetype in which all strings
68
	 * returned from the convert*() methods should be used,
69
	 * e.g. "text/xml".
70
	 *
71
	 * @var string
72
	 */
73
	protected $outputContentType = null;
74
75
	/**
76
	 * Used to set totalSize properties on the output
77
	 * of {@link convertDataObjectSet()}, shows the
78
	 * total number of records without the "limit" and "offset"
79
	 * GET parameters. Useful to implement pagination.
80
	 *
81
	 * @var int
82
	 */
83
	protected $totalSize;
84
85
	/**
86
	 * Get a DataFormatter object suitable for handling the given file extension.
87
	 *
88
	 * @param string $extension
89
	 * @return DataFormatter
90
	 */
91 View Code Duplication
	public static function for_extension($extension) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
92
		$classes = ClassInfo::subclassesFor("DataFormatter");
93
		array_shift($classes);
94
		$sortedClasses = array();
95
		foreach($classes as $class) {
96
			$sortedClasses[$class] = singleton($class)->stat('priority');
97
		}
98
		arsort($sortedClasses);
99
		foreach($sortedClasses as $className => $priority) {
100
			$formatter = new $className();
101
			if(in_array($extension, $formatter->supportedExtensions())) {
102
				return $formatter;
103
			}
104
		}
105
	}
106
107
	/**
108
	 * Get formatter for the first matching extension.
109
	 *
110
	 * @param array $extensions
111
	 * @return DataFormatter
112
	 */
113
	public static function for_extensions($extensions) {
114
		foreach($extensions as $extension) {
115
			if($formatter = self::for_extension($extension)) return $formatter;
116
		}
117
118
		return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by DataFormatter::for_extensions of type DataFormatter.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
119
	}
120
121
	/**
122
	 * Get a DataFormatter object suitable for handling the given mimetype.
123
	 *
124
	 * @param string $mimeType
125
	 * @return DataFormatter
126
	 */
127 View Code Duplication
	public static function for_mimetype($mimeType) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
128
		$classes = ClassInfo::subclassesFor("DataFormatter");
129
		array_shift($classes);
130
		$sortedClasses = array();
131
		foreach($classes as $class) {
132
			$sortedClasses[$class] = singleton($class)->stat('priority');
133
		}
134
		arsort($sortedClasses);
135
		foreach($sortedClasses as $className => $priority) {
136
			$formatter = new $className();
137
			if(in_array($mimeType, $formatter->supportedMimeTypes())) {
138
				return $formatter;
139
			}
140
		}
141
	}
142
143
	/**
144
	 * Get formatter for the first matching mimetype.
145
	 * Useful for HTTP Accept headers which can contain
146
	 * multiple comma-separated mimetypes.
147
	 *
148
	 * @param array $mimetypes
149
	 * @return DataFormatter
150
	 */
151
	public static function for_mimetypes($mimetypes) {
152
		foreach($mimetypes as $mimetype) {
153
			if($formatter = self::for_mimetype($mimetype)) return $formatter;
154
		}
155
156
		return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by DataFormatter::for_mimetypes of type DataFormatter.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
157
	}
158
159
	/**
160
	 * @param array $fields
161
	 */
162
	public function setCustomFields($fields) {
163
		$this->customFields = $fields;
164
	}
165
166
	/**
167
	 * @return array
168
	 */
169
	public function getCustomFields() {
170
		return $this->customFields;
171
	}
172
173
	/**
174
	 * @param array $fields
175
	 */
176
	public function setCustomAddFields($fields) {
177
		$this->customAddFields = $fields;
178
	}
179
180
	/**
181
	 * @param array $relations
182
	 */
183
	public function setCustomRelations($relations) {
184
		$this->customRelations = $relations;
185
	}
186
187
	/**
188
	 * @return array
189
	 */
190
	public function getCustomRelations() {
191
		return $this->customRelations;
192
	}
193
194
	/**
195
	 * @return array
196
	 */
197
	public function getCustomAddFields() {
198
		return $this->customAddFields;
199
	}
200
201
	/**
202
	 * @param array $fields
203
	 */
204
	public function setRemoveFields($fields) {
205
		$this->removeFields = $fields;
206
	}
207
208
	/**
209
	 * @return array
210
	 */
211
	public function getRemoveFields() {
212
		return $this->removeFields;
213
	}
214
215
	public function getOutputContentType() {
216
		return $this->outputContentType;
217
	}
218
219
	/**
220
	 * @param int $size
221
	 */
222
	public function setTotalSize($size) {
223
		$this->totalSize = (int)$size;
224
	}
225
226
	/**
227
	 * @return int
228
	 */
229
	public function getTotalSize() {
230
		return $this->totalSize;
231
	}
232
233
	/**
234
	 * Returns all fields on the object which should be shown
235
	 * in the output. Can be customised through {@link self::setCustomFields()}.
236
	 *
237
	 * @todo Allow for custom getters on the processed object (currently filtered through inheritedDatabaseFields)
238
	 * @todo Field level permission checks
239
	 *
240
	 * @param DataObject $obj
241
	 * @return array
242
	 */
243
	protected function getFieldsForObj($obj) {
244
		$dbFields = array();
245
246
		// if custom fields are specified, only select these
247
		if(is_array($this->customFields)) {
248 View Code Duplication
			foreach($this->customFields as $fieldName) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
249
				// @todo Possible security risk by making methods accessible - implement field-level security
250
				if($obj->hasField($fieldName) || $obj->hasMethod("get{$fieldName}")) {
251
					$dbFields[$fieldName] = $fieldName;
252
				}
253
			}
254
		} else {
255
			// by default, all database fields are selected
256
			$dbFields = $obj->inheritedDatabaseFields();
257
		}
258
259
		if(is_array($this->customAddFields)) {
260 View Code Duplication
			foreach($this->customAddFields as $fieldName) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
261
				// @todo Possible security risk by making methods accessible - implement field-level security
262
				if($obj->hasField($fieldName) || $obj->hasMethod("get{$fieldName}")) {
263
					$dbFields[$fieldName] = $fieldName;
264
				}
265
			}
266
		}
267
268
		// add default required fields
269
		$dbFields = array_merge($dbFields, array('ID'=>'Int'));
270
271
		if(is_array($this->removeFields)) {
272
			$dbFields = array_diff_key($dbFields, array_combine($this->removeFields,$this->removeFields));
273
		}
274
275
		return $dbFields;
276
	}
277
278
	/**
279
	 * Return an array of the extensions that this data formatter supports
280
	 */
281
	abstract public function supportedExtensions();
282
283
	abstract public function supportedMimeTypes();
284
285
286
	/**
287
	 * Convert a single data object to this format.  Return a string.
288
	 */
289
	abstract public function convertDataObject(DataObjectInterface $do);
290
291
	/**
292
	 * Convert a data object set to this format.  Return a string.
293
	 */
294
	abstract public function convertDataObjectSet(SS_List $set);
295
296
	/**
297
	 * @param string $strData HTTP Payload as string
298
	 */
299
	public function convertStringToArray($strData) {
300
		user_error('DataFormatter::convertStringToArray not implemented on subclass', E_USER_ERROR);
301
	}
302
303
}
304