Issues (6)

src/Response.php (3 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Micro
7
 *
8
 * @author      Raffael Sahli <[email protected]>
9
 * @copyright   Copryright (c) 2015-2018 gyselroth GmbH (https://gyselroth.com)
10
 * @license     MIT https://opensource.org/licenses/MIT
11
 */
12
13
namespace Micro\Http;
14
15
use Closure;
16
use SimpleXMLElement;
17
18
class Response
19
{
20
    /**
21
     * Possible output formats.
22
     */
23
    const OUTPUT_FORMATS = [
24
        'json' => 'application/json; charset=utf-8',
25
        'xml' => 'application/xml; charset=utf-8',
26
        'text' => 'text/html; charset=utf-8',
27
    ];
28
    /**
29
     * Output format.
30
     *
31
     * @var string
32
     */
33
    protected $output_format = 'json';
34
35
    /**
36
     * Human readable output.
37
     *
38
     * @var bool
39
     */
40
    protected $pretty_format = false;
41
42
    /**
43
     * Headers.
44
     *
45
     * @var array
46
     */
47
    protected $headers = [];
48
49
    /**
50
     * Code.
51
     *
52
     * @var int
53
     */
54
    protected $code = 200;
55
56
    /**
57
     * Body.
58
     *
59
     * @var string
60
     */
61
    protected $body;
62
63
    /**
64
     * Init response.
65
     */
66
    public function __construct()
67
    {
68
        $this->setupFormats();
69
    }
70
71
    /**
72
     * Set header.
73
     *
74
     * @param string $header
75
     *
76
     * @return Response
77
     */
78
    public function setHeader(string $header, string $value): self
79
    {
80
        $this->headers[$header] = $value;
81
82
        return $this;
83
    }
84
85
    /**
86
     * Delete header.
87
     *
88
     * @param string $header
89
     *
90
     * @return Response
91
     */
92
    public function removeHeader(string $header): self
93
    {
94
        if (isset($this->headers[$header])) {
95
            unset($this->headers[$header]);
96
        }
97
98
        return $this;
99
    }
100
101
    /**
102
     * Get headers.
103
     *
104
     * @return array
105
     */
106
    public function getHeaders(): array
107
    {
108
        return $this->headers;
109
    }
110
111
    /**
112
     * Send headers.
113
     *
114
     * @return Response
115
     */
116
    public function sendHeaders(): self
117
    {
118
        foreach ($this->headers as $header => $value) {
119
            header($header.': '.$value);
120
        }
121
122
        return $this;
123
    }
124
125
    /**
126
     * Set response code.
127
     *
128
     * @param int $code
129
     *
130
     * @return Response
131
     */
132
    public function setCode(int $code): self
133
    {
134
        if (!array_key_exists($code, Http::STATUS_CODES)) {
135
            throw new Exception('invalid http code set');
136
        }
137
138
        $this->code = $code;
139
140
        return $this;
141
    }
142
143
    /**
144
     * Get response code.
145
     *
146
     * @return int
147
     */
148
    public function getCode(): int
149
    {
150
        return $this->code;
151
    }
152
153
    /**
154
     * Set body.
155
     *
156
     * @param mixed $body
157
     *
158
     * @return Response
159
     */
160
    public function setBody($body): self
161
    {
162
        $this->body = $body;
163
164
        return $this;
165
    }
166
167
    /**
168
     * Get body.
169
     *
170
     * @return string
171
     */
172
    public function getBody()
173
    {
174
        return $this->body;
175
    }
176
177
    /**
178
     * Sends the actual response.
179
     */
180
    public function send(): void
181
    {
182
        $status = Http::STATUS_CODES[$this->code];
183
        $this->sendHeaders();
184
        header('HTTP/1.0 '.$this->code.' '.$status, true, $this->code);
185
186
        if (null === $this->body || 204 === $this->code) {
187
            return;
188
        }
189
190
        if ($this->body instanceof Closure) {
0 ignored issues
show
$this->body is never a sub-type of Closure.
Loading history...
191
            $body = $this->body->call($this);
0 ignored issues
show
The assignment to $body is dead and can be removed.
Loading history...
192
193
            return;
194
        }
195
        $body = $this->body;
196
197
        switch ($this->output_format) {
198
            case null:
199
            break;
200
            default:
201
            case 'json':
202
                echo $this->asJSON($body);
203
204
            break;
205
            case 'xml':
206
                echo $this->asXML($body);
207
208
            break;
209
            case 'text':
210
                echo $body;
211
212
            break;
213
        }
214
    }
215
216
    /**
217
     * Get output format.
218
     *
219
     * @return string
220
     */
221
    public function getOutputFormat(): string
222
    {
223
        return $this->output_format;
224
    }
225
226
    /**
227
     * Convert response to human readable output.
228
     *
229
     * @param bool $format
230
     *
231
     * @return Response
232
     */
233
    public function setPrettyFormat(bool $format): self
234
    {
235
        $this->pretty_format = (bool) $format;
236
237
        return $this;
238
    }
239
240
    /**
241
     * Set header Content-Length $body.
242
     *
243
     * @param string $body
244
     *
245
     * @return Response
246
     */
247
    public function setContentLength(string $body): self
248
    {
249
        header('Content-Length: '.strlen($body));
250
251
        return $this;
252
    }
253
254
    /**
255
     * Converts $body to pretty json.
256
     *
257
     * @param mixed $body
258
     *
259
     * @return string
260
     */
261
    public function asJSON($body): string
262
    {
263
        if ($this->pretty_format) {
264
            $result = json_encode($body, JSON_PRETTY_PRINT);
265
        } else {
266
            $result = json_encode($body);
267
        }
268
269
        if (false === $result) {
270
            return '';
271
        }
272
273
        $this->setContentLength($result);
274
275
        return $result;
276
    }
277
278
    /**
279
     * Converts mixed data to XML.
280
     *
281
     * @param mixed            $data
282
     * @param SimpleXMLElement $xml
283
     * @param string           $child_name
284
     *
285
     * @return string
286
     */
287
    public function toXML($data, SimpleXMLElement $xml, string $child_name): string
288
    {
289
        if (is_array($data)) {
290
            foreach ($data as $k => $v) {
291
                if (is_array($v)) {
292
                    (is_int($k)) ? $this->toXML($v, $xml->addChild($child_name), $v) : $this->toXML($v, $xml->addChild(strtolower($k)), $child_name);
0 ignored issues
show
$v of type array is incompatible with the type string expected by parameter $child_name of Micro\Http\Response::toXML(). ( Ignorable by Annotation )

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

292
                    (is_int($k)) ? $this->toXML($v, $xml->addChild($child_name), /** @scrutinizer ignore-type */ $v) : $this->toXML($v, $xml->addChild(strtolower($k)), $child_name);
Loading history...
293
                } else {
294
                    (is_int($k)) ? $xml->addChild($child_name, $v) : $xml->addChild(strtolower($k), $v);
295
                }
296
            }
297
        } else {
298
            $xml->addChild($child_name, $data);
299
        }
300
301
        return $xml->asXML();
302
    }
303
304
    /**
305
     * Converts response to xml.
306
     *
307
     * @param mixed $body
308
     *
309
     * @return string
310
     */
311
    public function asXML($body): string
312
    {
313
        $root = new SimpleXMLElement('<response></response>');
314
        $raw = $this->toXML($body, $root, 'node');
315
316
        if ($this->pretty_format) {
317
            $raw = $this->prettyXml($raw);
318
        }
319
320
        $this->setContentLength($raw);
321
322
        return $raw;
323
    }
324
325
    /**
326
     * Pretty formatted xml.
327
     *
328
     * @param string $xml
329
     *
330
     * @return string
331
     */
332
    public function prettyXml(string $xml): string
333
    {
334
        $domxml = new \DOMDocument('1.0');
335
        $domxml->preserveWhiteSpace = false;
336
        $domxml->formatOutput = true;
337
        $domxml->loadXML($xml);
338
339
        return $domxml->saveXML();
340
    }
341
342
    /**
343
     * Set the current output format.
344
     *
345
     * @param string $format
346
     *
347
     * @return Response
348
     */
349
    public function setOutputFormat(?string $format = null): self
350
    {
351
        if (null === $format) {
352
            $this->output_format = null;
353
354
            return $this;
355
        }
356
357
        if (!array_key_exists($format, self::OUTPUT_FORMATS)) {
358
            throw new Exception('invalid output format given');
359
        }
360
361
        $this->setHeader('Content-Type', self::OUTPUT_FORMATS[$format]);
362
        $this->output_format = $format;
363
364
        return $this;
365
    }
366
367
    /**
368
     * Setup formats.
369
     *
370
     * @return Response
371
     */
372
    public function setupFormats(): self
373
    {
374
        $pretty = array_key_exists('pretty', $_GET) && ('false' !== $_GET['pretty'] && '0' !== $_GET['pretty']);
375
        $this->setPrettyFormat($pretty);
376
377
        //through HTTP_ACCEPT
378
        if (isset($_SERVER['HTTP_ACCEPT']) && false === strpos($_SERVER['HTTP_ACCEPT'], '*/*')) {
379
            foreach (self::OUTPUT_FORMATS as $format => $type) {
380
                if (false !== strpos($_SERVER['HTTP_ACCEPT'], $type)) {
381
                    $this->output_format = $format;
382
383
                    break;
384
                }
385
            }
386
        }
387
388
        $this->setOutputFormat($this->output_format);
389
390
        return $this;
391
    }
392
}
393