Failed Conditions
Push — master ( c59a50...787d16 )
by Arnold
02:51
created

Output::ok()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 1
1
<?php
2
3
namespace Jasny\Controller;
4
5
use Psr\Http\Message\ResponseInterface;
6
use Dflydev\ApacheMimeTypes\PhpRepository as ApacheMimeTypes;
7
8
/**
9
 * Methods for a controller to send a response
10
 */
11
trait Output
12
{
13
    /**
14
     * @var string
15
     */
16
    protected $defaultFormat;
17
    
18
    /**
19
     * Get response. set for controller
20
     *
21
     * @return ResponseInterface
22
     */
23
    abstract public function getResponse();
24
25
    /**
26
     * Get response. set for controller
27
     *
28
     * @return ResponseInterface
29
     */
30
    abstract public function setResponse(ResponseInterface $response);
31
32
    /**
33
     * Returns the HTTP referer if it is on the current host
34
     *
35
     * @return string
36
     */
37
    abstract public function getLocalReferer();
38
    
39
    
40
    /**
41
     * Set a response header
42
     * 
43
     * @param string  $header
44
     * @param string  $value
45
     * @param boolean $overwrite
46
     */
47 7
    public function setResponseHeader($header, $value, $overwrite = true)
48
    {
49 7
        $fn = $overwrite ? 'withHeader' : 'withAddedHeader';
50 7
        $response = $this->getResponse()->$fn($header, $value);
51
        
52 7
        $this->setResponse($response);
53 7
    }
54
    
55
    /**
56
     * Set the headers with HTTP status code and content type.
57
     * @link http://en.wikipedia.org/wiki/List_of_HTTP_status_codes
58
     * 
59
     * Examples:
60
     * <code>
61
     *   $this->respondWith(200, 'json');
62
     *   $this->respondWith(200, 'application/json');
63
     *   $this->respondWith(204);
64
     *   $this->respondWith("204 Created");
65
     *   $this->respondWith('json');
66
     * </code>
67
     * 
68
     * @param int|string   $status  HTTP status (may be omitted)
69
     * @param string|array $format  Mime or content format
70
     */
71 46
    public function respondWith($status, $format = null)
72
    {
73 46
        $response = $this->getResponse();
74
75
        // Shift arguments if $code is omitted
76 46
        if (isset($status) && !is_int($status) && (!is_string($status) || !preg_match('/^\d{3}\b/', $status))) {
77 4
            list($status, $format) = array_merge([null], func_get_args());
78 4
        }
79
80 46
        if (!empty($status)) {
81 40
            list($code, $phrase) = explode(' ', $status, 2) + [1 => null];
82 40
            $response = $response->withStatus((int)$code, $phrase);
83 40
        }
84
        
85 46
        if (!empty($format)) {
86 8
            $contentType = $this->getContentType($format);
87 7
            $response = $response->withHeader('Content-Type', $contentType);   
88 7
        }
89
90 45
        $this->setResponse($response);
91 45
    }
92
93
    
94
    /**
95
     * Response with 200 OK
96
     *
97
     * @return ResponseInterface $response
98
     */
99 1
    public function ok()
100
    {
101 1
        $this->respondWith(200);
102 1
    }
103
104
    /**
105
     * Response with created 201 code, and optionally the created location
106
     *
107
     * @param string $location  Url of created resource
108
     */
109 2
    public function created($location = null)
110
    {
111 2
        $this->respondWith(201);
112
113 2
        if (!empty($location)) {
114 1
            $this->setResponseHeader('Location', $location);
115 1
        }
116 2
    }
117
118
    /**
119
     * Response with 203 Accepted
120
     */
121 1
    public function accepted()
122
    {
123 1
        $this->respondWith(202);
124 1
    }
125
126
    /**
127
     * Response with 204 No Content
128
     * 
129
     * @param int $code  204 (No Content) or 205 (Reset Content)
130
     */
131 2
    public function noContent($code = 204)
132
    {
133 2
        $this->respondWith($code);
134 2
    }
135
    
136
    /**
137
     * Respond with a 206 Partial content with `Content-Range` header
138
     * 
139
     * @param int $rangeFrom  Beginning of the range in bytes
140
     * @param int $rangeTo    End of the range in bytes
141
     * @param int $totalSize  Total size in bytes
142
     */
143 2
    public function partialContent($rangeFrom, $rangeTo, $totalSize)
144
    {
145 2
        $this->respondWith(206);
146
        
147 2
        $this->setResponseHeader('Content-Range', "bytes {$rangeFrom}-{$rangeTo}/{$totalSize}");
148 2
        $this->setResponseHeader('Content-Length', $rangeTo - $rangeFrom);
149 2
    }
150
151
    
152
    /**
153
     * Redirect to url and output a short message with the link
154
     *
155
     * @param string $url
156
     * @param int    $code  301 (Moved Permanently), 302 (Found), 303 (See Other) or 307 (Temporary Redirect)
157
     */
158 10
    public function redirect($url, $code = 303)
159
    {
160 10
        $this->respondWith($code);
161 10
        $this->setResponseHeader('Location', $url);
162
        
163 10
        $urlHtml = htmlentities($url);
164 10
        $this->output('You are being redirected to <a href="' . $urlHtml . '">' . $urlHtml . '</a>', 'text/html');
165 10
    }
166
167
    /**
168
     * Redirect to previous page, or to home page
169
     *
170
     * @return ResponseInterface $response
171
     */
172 4
    public function back()
173
    {
174 4
        $this->redirect($this->getLocalReferer() ?: '/');
175 4
    }
176
    
177
    /**
178
     * Respond with 304 Not Modified
179
     */
180 1
    public function notModified()
181
    {
182 1
        $this->respondWith(304);
183 1
    }
184
185
    
186
    /**
187
     * Respond with 400 Bad Request
188
     *
189
     * @param string $message
190
     * @param int    $code     HTTP status code
191
     */
192 3
    public function badRequest($message, $code = 400)
193
    {
194 3
        $this->respondWith($code);
195 3
        $this->output($message);
196 3
    }
197
198
    /**
199
     * Respond with a 401 Unauthorized
200
     */
201 2
    public function requireAuth()
202
    {
203 2
        $this->respondWith(401);
204 2
    }
205
206
    /**
207
     * Alias of requireAuth
208
     * @deprecated
209
     */
210 1
    final public function requireLogin()
211
    {
212 1
        $this->requireAuth();
213 1
    }
214
    
215
    /**
216
     * Respond with 402 Payment Required
217
     *
218
     * @param string $message
219
     */
220 3
    public function paymentRequired($message = "Payment required")
221
    {
222 3
        $this->respondWith(402);
223 3
        $this->output($message);
224 3
    }
225
226
    /**
227
     * Respond with 403 Forbidden
228
     *
229
     * @param string $message
230
     */
231 3
    public function forbidden($message = "Access denied")
232
    {
233 3
        $this->respondWith(403);
234 3
        $this->output($message);
235 3
    }
236
237
    /**
238
     * Respond with 404 Not Found
239
     *
240
     * @param string $message
241
     * @param int    $code     404 (Not Found), 405 (Method not allowed) or 410 (Gone)
242
     */
243 4
    public function notFound($message = "Not found", $code = 404)
244
    {
245 4
        $this->respondWith($code);
246 4
        $this->output($message);
247 4
    }
248
249
    /**
250
     * Respond with 409 Conflict
251
     *
252
     * @param string $message
253
     */
254 2
    public function conflict($message)
255
    {
256 2
        $this->respondWith(409);
257 2
        $this->output($message);
258 2
    }
259
260
    /**
261
     * Respond with 429 Too Many Requests
262
     *
263
     * @param string $message
264
     */
265 3
    public function tooManyRequests($message = "Too many requests")
266
    {
267 3
        $this->respondWith(429);
268 3
        $this->output($message);
269 3
    }
270
271
    
272
    /**
273
     * Respond with a server error
274
     *
275
     * @param string $message
276
     * @param int    $code     HTTP status code
277
     */
278 4
    public function error($message = "An unexpected error occured", $code = 500)
279
    {
280 4
        $this->respondWith($code);
281 4
        $this->output($message);
282 4
    }
283
    
284
    
285
    /**
286
     * Get MIME type for extension
287
     *
288
     * @param string $format
289
     * @return string
290
     */
291 35
    protected function getContentType($format)
292
    {
293 35
        if (\Jasny\str_contains($format, '/')) { // Already MIME
294 8
            return $format;
295
        }
296
        
297 27
        $repository = new ApacheMimeTypes();
298 27
        $mime = $repository->findType($format);
299
300 27
        if (!isset($mime)) {
301 1
            throw new \UnexpectedValueException("Format '$format' doesn't correspond with a MIME type");
302
        }
303
        
304 26
        return $mime;
305
    }
306
    
307
    
308
    /**
309
     * If a non scalar value is passed without an format, use this format
310
     * 
311
     * @param string $format  Format by extention or MIME
312
     */
313 18
    public function byDefaultSerializeTo($format)
314
    {
315 18
        $this->defaultFormat = $format;
316 18
    }
317
    
318
    /**
319
     * Serialize data
320
     * 
321
     * @param mixed  $data
322
     * @param string $contentType
323
     * @return string
324
     */
325 52
    protected function serializeData($data, $contentType)
326
    {
327 52
        if (is_string($data)) {
328 32
            return $data;
329
        }
330
        
331 20
        $repository = new ApacheMimeTypes();
332 20
        list($format) = $repository->findExtensions($contentType) + [null];
333
        
334 20
        $method = 'serializeDataTo' . $format;
335
        
336 20
        if (method_exists($this, $method)) {
337 17
            return $this->$method($data);
338
        }
339
340 7
        $type = (is_object($data) ? get_class($data) . ' ' : '') . gettype($data);
341 7
        throw new \UnexpectedValueException("Unable to serialize $type to '$contentType'");
342
    }
343
    
344
    /**
345
     * Serialize data to JSON
346
     * @internal made private because this will likely move to a library
347
     * 
348
     * @param mixed $data
349
     * @return string
350
     */
351 4
    private function serializeDataToJson($data)
352
    {
353 4
        return json_encode($data);
354
    }
355
    
356
    /**
357
     * Serialize data to XML.
358
     * @internal made private because this will likely move to a library
359
     * 
360
     * @param mixed $data
361
     * @return string
362
     */
363 13
    private function serializeDataToXml($data)
364
    {
365 13
        if ($data instanceof \SimpleXMLElement) {
366 4
            return $data->asXML();
367
        }
368
        
369 9
        if (($data instanceof \DOMNode && isset($data->ownerDocument)) || $data instanceof \DOMDocument) {
370 8
            $dom = $data instanceof \DOMDocument ? $data : $data->ownerDocument;
371 8
            return $dom->saveXML($data);
372
        }
373
        
374 1
        $type = (is_object($data) ? get_class($data) . ' ' : '') . gettype($data);
375 1
        throw new \UnexpectedValueException("Unable to serialize $type to XML");
376
    }
377
378
    /**
379
     * Output result
380
     *
381
     * @param mixed  $data
382
     * @param string $format  Output format as MIME or extension
383
     */
384 52
    public function output($data, $format = null)
385
    {
386 52
        if (!isset($format)) {
387 41
            $contentType = $this->getResponse()->getHeaderLine('Content-Type');
388
            
389 41
            if (empty($contentType)) {
390 12
                $format = $this->defaultFormat ?: 'text/html';
391 12
            }
392 41
        }
393
        
394 52
        if (empty($contentType)) {
395 27
            $contentType = $this->getContentType($format);
396 27
            $this->setResponseHeader('Content-Type', $contentType);
397 27
        }
398
399
        try {
400 52
            $content = $this->serializeData($data, $contentType);
401 52
        } catch (\UnexpectedValueException $e) {
402 8
            if (!isset($format) && isset($this->defaultFormat) && $this->defaultFormat !== $contentType) {
403 4
                $this->output($data, $this->defaultFormat); // Try default format instead
404 4
                return;
405
            }
406
            
407 4
            throw $e;
408
        }
409
410 48
        $this->getResponse()->getBody()->write($content);
411 48
    }
412
}
413