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
![]() |
|||||
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
|
|||||
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
|
|||||
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
|
|||||
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
|
|||||
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
|
|||||
332 | $dbFields = array_diff_key($dbFields, array_combine($this->removeFields, $this->removeFields)); |
||||
0 ignored issues
–
show
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
![]() |
|||||
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
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
This check looks for parameters that have been defined for a function or method, but which are not used in the method body. ![]() |
|||||
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
|
|||||
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
|
|||||
451 | if (array_key_exists($key, $map)) { |
||||
452 | return $map[$key]; |
||||
453 | } else { |
||||
454 | return $key; |
||||
455 | } |
||||
456 | } |
||||
457 | return $key; |
||||
458 | } |
||||
459 | } |
||||
460 |