1 <?php
2 /**
3 * A DataFormatter object handles transformation of data from Sapphire model objects to a particular output format, and vice versa.
4 * This is most commonly used in developing RESTful APIs.
5 *
6 * @package sapphire
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 public 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 static function for_extension($extension) {
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 static function for_extensions($extensions) {
114 foreach($extensions as $extension) {
115 if($formatter = self::for_extension($extension)) return $formatter;
116 }
117
118 return false;
119 }
120
121 /**
122 * Get a DataFormatter object suitable for handling the given mimetype.
123 *
124 * @param string $mimeType
125 * @return DataFormatter
126 */
127 static function for_mimetype($mimeType) {
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 static function for_mimetypes($mimetypes) {
152 foreach($mimetypes as $mimetype) {
153 if($formatter = self::for_mimetype($mimetype)) return $formatter;
154 }
155
156 return false;
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 foreach($this->customFields as $fieldName) {
249 // @todo Possible security risk by making methods accessible - implement field-level security
250 if($obj->hasField($fieldName) || $obj->hasMethod("get{$fieldName}")) $dbFields[$fieldName] = $fieldName;
251 }
252 } else {
253 // by default, all database fields are selected
254 $dbFields = $obj->inheritedDatabaseFields();
255 }
256
257 if(is_array($this->customAddFields)) {
258 foreach($this->customAddFields as $fieldName) {
259 // @todo Possible security risk by making methods accessible - implement field-level security
260 if($obj->hasField($fieldName) || $obj->hasMethod("get{$fieldName}")) $dbFields[$fieldName] = $fieldName;
261 }
262 }
263
264 // add default required fields
265 $dbFields = array_merge($dbFields, array('ID'=>'Int'));
266
267 // @todo Requires PHP 5.1+
268 if(is_array($this->removeFields)) {
269 $dbFields = array_diff_key($dbFields, array_combine($this->removeFields,$this->removeFields));
270 }
271
272 return $dbFields;
273 }
274
275 /**
276 * Return an array of the extensions that this data formatter supports
277 */
278 abstract function supportedExtensions();
279
280 abstract function supportedMimeTypes();
281
282
283 /**
284 * Convert a single data object to this format. Return a string.
285 */
286 abstract function convertDataObject(DataObjectInterface $do);
287
288 /**
289 * Convert a data object set to this format. Return a string.
290 */
291 abstract function convertDataObjectSet(DataObjectSet $set);
292
293 /**
294 * @param string $strData HTTP Payload as string
295 */
296 public function convertStringToArray($strData) {
297 user_error('DataFormatter::convertStringToArray not implemented on subclass', E_USER_ERROR);
298 }
299
300 }