1 <?php
2
3 /*
4 * This file is part of the ICanBoogie package.
5 *
6 * (c) Olivier Laviale <olivier.laviale@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace ICanBoogie\Operation;
13
14 use ICanBoogie\Errors;
15
16 /**
17 * @property string $message The response message.
18 * @property-read \ICanBoogie\Errors $errors
19 */
20 class Response extends \ICanBoogie\HTTP\Response implements \ArrayAccess
21 {
22 /**
23 * Result of the response.
24 *
25 * @var mixed
26 */
27 public $rc;
28
29 /**
30 * Message associated with the response.
31 *
32 * @var string|array
33 */
34 private $message;
35
36 /**
37 * Sets the response message.
38 *
39 * @param string $message
40 *
41 * @throws \InvalidArgumentException if the message is an array or an object that do not implement `__toString()`.
42 */
43 protected function set_message($message)
44 {
45 if (is_array($message) || (is_object($message) && !method_exists($message, '__toString')))
46 {
47 throw new \InvalidArgumentException(\ICanBoogie\format
48 (
49 'Invalid message type "{0}", shoud be a string or an object implementing "__toString()". Given: {1}', array
50 (
51 gettype($message), $message
52 )
53 ));
54 }
55
56 $this->message = $message;
57 }
58
59 /**
60 * Returns the response message.
61 *
62 * @return string
63 */
64 protected function get_message()
65 {
66 return $this->message;
67 }
68
69 /**
70 * Errors occuring during the response.
71 *
72 * @var Errors
73 */
74 public $errors;
75
76 protected $metas = array();
77
78 /**
79 * Initializes the {@link $errors} property.
80 *
81 * @see \ICanBoogie\HTTP\Response::__construct
82 */
83 public function __construct($body=null, $status=200, array $headers=array())
84 {
85 parent::__construct($body, $status, $headers);
86
87 $this->errors = new Errors();
88 }
89
90 public function __invoke()
91 {
92 if ($this->body !== null)
93 {
94 return parent::__invoke();
95 }
96
97 $body_data = array();
98
99 # rc
100
101 $rc = null;
102
103 if ($this->is_successful)
104 {
105 $rc = $this->rc;
106
107 if (is_object($rc) && method_exists($rc, '__toString'))
108 {
109 $rc = (string) $rc;
110 }
111
112 $body_data['rc'] = $rc;
113 }
114
115 # message
116
117 $message = $this->message;
118
119 if ($message !== null)
120 {
121 $body_data['message'] = (string) $message;
122 }
123
124 # errors
125
126 if (isset($this->errors) && count($this->errors))
127 {
128 $errors = array();
129
130 foreach ($this->errors as $identifier => $error)
131 {
132 if (!$identifier)
133 {
134 $identifier = '_base';
135 }
136
137 if (isset($errors[$identifier]))
138 {
139 $errors[$identifier] .= '; ' . $error;
140 }
141 else
142 {
143 $errors[$identifier] = is_bool($error) ? $error : (string) $error;
144 }
145 }
146
147 $body_data['errors'] = $errors;
148 }
149
150 # metas
151
152 $body_data += $this->metas;
153
154 /*
155 * If a location is set on the request it is remove and added to the result message.
156 * This is because if we use XHR to get the result we don't want that result to go
157 * avail because the operation usually change the location.
158 *
159 * TODO-20110924: for XHR instead of JSON/XML.
160 */
161 if ($this->location)
162 {
163 $body_data['location'] = $this->location;
164
165 $this->location = null;
166 }
167
168 // FIXME: $rc only if valid !!!
169
170 if (is_array($rc) && !((string) $this->content_type)) // FIXME-20120107: must force 'application/json' for arrays... that might not be fair.
171 {
172 $this->content_type = 'application/json';
173 }
174
175 $body = $rc;
176
177 if ($this->content_type == 'application/json')
178 {
179 $body = json_encode($body_data);
180 $this->content_length = null;
181 }
182 else if ($this->content_type == 'application/xml')
183 {
184 $body = array_to_xml($body_data, 'response');
185 $this->content_length = null;
186 }
187
188 if ($this->content_length === null && is_string($body))
189 {
190 $this->set_content_length(strlen($body));
191 }
192
193 $this->body = $body;
194
195 return parent::__invoke();
196 }
197
198 /*
199 * TODO-20110923: we used to return *all* the fields of the response, we can't do this anymore
200 * because most of this stuff belongs to the response object. We need a mean to add
201 * additional properties, and maybe we could use the response as an array for this purpose:
202 *
203 * Example:
204 *
205 * $response->rc = true;
206 * $response['widget'] = (string) new Button('madonna');
207 *
208 * This might be better than $response->additional_results->widget = ...;
209 *
210 * Or we could let that behind us and force everything in the `rc`:
211 *
212 * rc: {
213 *
214 * widget: '<div class="widget-pop-node">...</div>',
215 * assets: {
216 *
217 * css: [...],
218 * js: [...]
219 * }
220 * }
221 *
222 * We could also drop 'rc' because it was used to check if the operation was
223 * successful (before we handle HTTP status correctly), we might not need it anymore.
224 *
225 * If the operation returns anything but an array, it is converted into an array with the 'rc'
226 * key and the value, 'success' and 'errors' are added if needed. This only apply to XHR
227 * request !!
228 *
229 */
230
231 /**
232 * Checks if a meta exists.
233 *
234 * @see ArrayAccess::offsetExists()
235 */
236 public function offsetExists($offset)
237 {
238 return isset($this->metas[$offset]);
239 }
240
241 /**
242 * Returns a meta or null if it is not defined.
243 *
244 * @see ArrayAccess::offsetGet()
245 */
246 public function offsetGet($offset)
247 {
248 return $this->offsetExists($offset) ? $this->metas[$offset] : null;
249 }
250
251 /**
252 * Sets a meta.
253 *
254 * @see ArrayAccess::offsetSet()
255 */
256 public function offsetSet($offset, $value)
257 {
258 $this->metas[$offset] = $value;
259 }
260
261 /**
262 * Unsets a meta.
263 *
264 * @see ArrayAccess::offsetUnset()
265 */
266 public function offsetUnset($offset)
267 {
268 unset($this->metas[$offset]);
269 }
270 }
271
272 function array_to_xml($array, $parent='root', $encoding='utf-8', $nest=1)
273 {
274 $rc = '';
275
276 if ($nest == 1)
277 {
278 #
279 # first level, time to write the XML header and open the root markup
280 #
281
282 $rc .= '<?xml version="1.0" encoding="' . $encoding . '"?>' . PHP_EOL;
283 $rc .= '<' . $parent . '>' . PHP_EOL;
284 }
285
286 $tab = str_repeat("\t", $nest);
287
288 if (substr($parent, -3, 3) == 'ies')
289 {
290 $collection = substr($parent, 0, -3) . 'y';
291 }
292 else if (substr($parent, -2, 2) == 'es')
293 {
294 $collection = substr($parent, 0, -2);
295 }
296 else if (substr($parent, -1, 1) == 's')
297 {
298 $collection = substr($parent, 0, -1);
299 }
300 else
301 {
302 $collection = 'entry';
303 }
304
305 foreach ($array as $key => $value)
306 {
307 if (is_numeric($key))
308 {
309 $key = $collection;
310 }
311
312 if (is_array($value) || is_object($value))
313 {
314 $rc .= $tab . '<' . $key . '>' . PHP_EOL;
315 $rc .= wd_array_to_xml((array) $value, $key, $encoding, $nest + 1);
316 $rc .= $tab . '</' . $key . '>' . PHP_EOL;
317
318 continue;
319 }
320
321 #
322 # if we find special chars, we put the value into a CDATA section
323 #
324
325 if (strpos($value, '<') !== false || strpos($value, '>') !== false || strpos($value, '&') !== false)
326 {
327 $value = '<![CDATA[' . $value . ']]>';
328 }
329
330 $rc .= $tab . '<' . $key . '>' . $value . '</' . $key . '>' . PHP_EOL;
331 }
332
333 if ($nest == 1)
334 {
335 #
336 # first level, time to close the root markup
337 #
338
339 $rc .= '</' . $parent . '>';
340 }
341
342 return $rc;
343 }