1 <?php namespace Modulework\Modules\Http;
2 3 4 5 6
7
8 use DateTime;
9 use DateTimeZone;
10 use RunTimeException;
11 use InvalidArgumentException;
12 use Modulework\Modules\Http\Cookie;
13 use Modulework\Modules\Http\Request;
14 use Modulework\Modules\Http\Utilities\ArrayCase;
15 use Modulework\Modules\Http\Utilities\HeaderCase;
16 use Modulework\Modules\Http\Utilities\HeaderWrapper;
17 use Modulework\Modules\Http\Utilities\HeaderWrapperInterface;
18 use Modulework\Modules\Http\Exceptions\HttpExceptionInterface;
19
20
21 22 23 24 25
26 class Response
27 {
28
29 30 31 32
33 protected $content;
34
35 36 37 38
39 protected $statusCode;
40
41 42 43 44
45 protected $statusText;
46
47 48 49 50
51 protected $charset;
52
53 54 55 56
57 protected $protocolVersion = '1.0';
58
59 60 61 62 63
64 public static $statusCodeRegistry = array(
65 100 => 'Continue',
66 101 => 'Switching Protocols',
67 102 => 'Processing',
68 200 => 'OK',
69 201 => 'Created',
70 202 => 'Accepted',
71 203 => 'Non-Authoritative Information',
72 204 => 'No Content',
73 205 => 'Reset Content',
74 206 => 'Partial Content',
75 207 => 'Multi-Status',
76 208 => 'Already Reported',
77 226 => 'IM Used',
78 300 => 'Multiple Choices',
79 301 => 'Moved Permanently',
80 302 => 'Found',
81 303 => 'See Other',
82 304 => 'Not Modified',
83 305 => 'Use Proxy',
84 306 => 'Reserved',
85 307 => 'Temporary Redirect',
86 308 => 'Permanent Redirect',
87 400 => 'Bad Request',
88 401 => 'Unauthorized',
89 402 => 'Payment Required',
90 403 => 'Forbidden',
91 404 => 'Not Found',
92 405 => 'Method Not Allowed',
93 406 => 'Not Acceptable',
94 407 => 'Proxy Authentication Required',
95 408 => 'Request Timeout',
96 409 => 'Conflict',
97 410 => 'Gone',
98 411 => 'Length Required',
99 412 => 'Precondition Failed',
100 413 => 'Request Entity Too Large ',
101 414 => 'Request-URI Too Long',
102 415 => 'Unsupported Media Type',
103 416 => 'Requested Range Not Satisfiable',
104 417 => 'Expectation Failed',
105 422 => 'Unprocessable Entity',
106 423 => 'Locked',
107 424 => 'Failed Dependency ',
108 425 => 'Unassigned',
109 426 => 'Upgrade Required',
110 427 => 'Unassigned',
111 428 => 'Precondition Required',
112 429 => 'Too Many Requests',
113 430 => 'Unassigned',
114 431 => 'Request Header Fields Too Large ',
115 500 => 'Internal Server Error',
116 501 => 'Not Implemented',
117 502 => 'Bad Gateway',
118 503 => 'Service Unavailable',
119 504 => 'Gateway Timeout',
120 505 => 'HTTP Version Not Supported',
121 506 => 'Variant Also Negotiates (Experimental)',
122 507 => 'Insufficient Storage',
123 508 => 'Loop Detected',
124 509 => 'Unassigned',
125 510 => 'Not Extended',
126 511 => 'Network Authentication Required'
127 );
128
129 130 131 132
133 public ;
134
135 136 137 138
139 public $cookies;
140
141 142 143 144
145 protected ;
146
147 148 149 150 151 152 153 154 155 156 157 158
159 public static function make($content = '', $code = 200, array $headers = array(), HeaderWrapperInterface $headerWrapper = null)
160 {
161 return new static($content, $code, $headers, $headerWrapper);
162 }
163
164 165 166 167 168 169 170 171 172 173 174 175
176 public static function fromException(HttpExceptionInterface $e, array $headers = array(), HeaderWrapperInterface $headerWrapper = null)
177 {
178 $content = isset(self::$statusCodeRegistry[$e->getStatusCode()]) ? self::$statusCodeRegistry[$e->getStatusCode()] : '';
179 $content = $e->getMessage() ?: $content;
180
181 return new static($content, $e->getStatusCode(), $headers, $headerWrapper);
182 }
183
184 185 186 187 188 189 190 191 192 193 194 195
196 public function __construct($content = '', $code = 200, array $headers = array(), HeaderWrapperInterface $headerWrapper = null)
197 {
198 $this->setStatusCode($code);
199 $this->setContent($content);
200
201 $this->headers = new HeaderCase($headers);
202 $this->cookies = new ArrayCase();
203
204 if (!$this->headers->has('Date')) {
205 $this->setDate(new DateTime(null, new DateTimeZone('UTC')));
206 }
207
208 $this->setHeaderWrapper($headerWrapper);
209 }
210
211 212 213 214 215 216 217 218 219
220 public function __toString()
221 {
222 return
223 sprintf('HTTP/%s %s %s', $this->getProtocolVersion(), $this->statusCode, $this->statusText) . "\r\n" .
224 $this->headers->showForResponse() . "\r\n" .
225 $this->getContent();
226 }
227
228 229 230 231 232 233 234 235 236 237
238 public function (HeaderWrapperInterface $headerWrapper = null)
239 {
240 if ($this->headerWrapper !== null) {
241 $ret = $this->headerWrapper;
242 } else {
243 $ret = null;
244 }
245
246 if ($headerWrapper === null) {
247 $this->headerWrapper = new HeaderWrapper;
248 } else {
249 $this->headerWrapper = $headerWrapper;
250 }
251
252 return $ret;
253 }
254
255 256 257 258 259
260 public function ()
261 {
262 if ($this->headerWrapper->headers_sent()) {
263 return $this;
264 }
265
266 $this->headerWrapper->header(sprintf('HTTP/%s %s %s', $this->getProtocolVersion(), $this->statusCode, $this->statusText));
267
268 foreach ($this->headers->all() as $name => $value) {
269 $this->headerWrapper->header($name . ': ' . $value);
270 }
271
272 $this->sendCookies();
273
274 return $this;
275
276 }
277
278 279 280 281
282 public function sendCookies()
283 {
284 if ($this->headerWrapper->headers_sent()) {
285 return $this;
286 }
287
288 foreach ($this->cookies->all() as $cookie) {
289 $this->headerWrapper->setcookie($cookie->getName(), $cookie->getValue(), $cookie->getExpiresTime(), $cookie->getPath(), $cookie->getDomain(), $cookie->isSecure(), $cookie->isHttpOnly());
290 }
291 return $this;
292 }
293
294 295 296 297
298 public function sendContent()
299 {
300 echo $this->content;
301
302 return $this;
303 }
304
305 306 307 308 309 310
311 public function send()
312 {
313 $this->sendHeaders();
314 $this->sendContent();
315
316 return $this;
317 }
318
319 320 321 322 323 324 325 326
327 public function prepare(Request $request)
328 {
329 if ($this->isInvalid()) {
330 throw new RunTimeException('Response is invalid (' . $this->statusCode . ')');
331
332 }
333
334 if ($this->isInformational()) {
335 $this->content = null;
336 }
337
338 $this->charset = $this->charset ?: 'UTF-8';
339 if (!$this->headers->has('Content-Type')) {
340 $this->headers->set('Content-Type', 'text/html; charset=' . $this->charset);
341 }
342
343
344
345
346
347 if ($this->statusCode === 200 && $this->headers->has('Location')) {
348 $this->setStatusCode('302');
349 }
350
351 if ($request->isMethod('HEAD')) {
352 $this->content = null;
353 }
354
355 if ('1.0' == $this->getProtocolVersion() && 'no-cache' == $this->headers->get('Cache-Control')) {
356 $this->headers->set('pragma', 'no-cache');
357 $this->headers->set('expires', -1);
358 }
359
360 return $this;
361 }
362
363 364 365 366
367 public function addCookie(Cookie $cookie)
368 {
369 $this->cookies->push($cookie);
370 }
371
372 373 374 375 376 377
378 public function ($name, $value, $overwrite = false)
379 {
380 $this->headers->set($name, $value, $overwrite);
381 }
382
383 384 385 386 387 388 389
390 public function setStatusCode($code = 200, $txt = null)
391 {
392 $this->statusCode = $code;
393
394 if ($txt === null) {
395 $this->statusText = isset(self::$statusCodeRegistry[$code]) ? self::$statusCodeRegistry[$code] : '';
396 return $this;
397 }
398
399 if ($txt === false) {
400 $this->statusText = '';
401 return $this;
402 }
403
404 $this->statusText = $txt;
405
406 return $this;
407 }
408
409 410 411 412
413 public function getStatusCode()
414 {
415 return $this->statusCode;
416 }
417
418 419 420 421 422 423 424
425 public function setContent($content = '')
426 {
427 if (!$this->validateContent($content)) {
428 throw new InvalidArgumentException('The Response content must be a string or an object implementing __toString() magic method, "' . gettype($content) . '" given.');
429 }
430
431 $this->content = (string) $content;
432
433 return $this;
434 }
435
436 437 438 439 440 441 442
443 public function appendContent($content = '')
444 {
445 if (!$this->validateContent($content)) {
446 throw new InvalidArgumentException('The Response content must be a string or an object implementing __toString() magic method, "' . gettype($content) . '" given.');
447 }
448
449 if ($this->content === '') {
450 return $this->setContent($content);
451 }
452
453 $this->content .= (string) $content;
454
455 return $this;
456 }
457 458 459 460
461 public function getContent()
462 {
463 return $this->content;
464 }
465
466 467 468 469 470
471 public function setDate(DateTime $date)
472 {
473 $this->headers->set('Date', $date->format('D, d M Y H:i:s') . ' GMT');
474 return $this;
475 }
476
477 478 479 480
481 public function getDate()
482 {
483 $default = new DateTime();
484 $default = $default->format('D, d M Y H:i:s') . 'GMT';
485
486 return $this->headers->get('Date', $default);
487 }
488
489 490 491 492 493
494 public function setProtocolVersion($version = '1.0')
495 {
496 $this->protocolVersion = $version;
497 return $this;
498 }
499
500 501 502 503
504 public function getProtocolVersion()
505 {
506 return $this->protocolVersion;
507 }
508
509 510 511 512 513 514 515
516 public function setExpires(DateTime $date = null)
517 {
518 if ($date === null) {
519 $this->headers->remove('Expires');
520 } else {
521 $date->setTimezone(new DateTimeZone('UTC'));
522 $this->headers->set('Expires', $date->format('D, d M Y H:i:s') . ' GMT');
523 }
524
525 return $this;
526 }
527
528 529 530 531 532
533 public function getExpires()
534 {
535 return new DateTime($this->headers->get('Expires'));
536 }
537
538 539 540 541 542
543 public function setCharset($charset)
544 {
545 $this->charset = $charset;
546 return $this;
547 }
548
549 550 551 552
553 public function getCharset()
554 {
555 return $this->charset;
556 }
557
558 559 560 561
562 public function isSuccess()
563 {
564 return ($this->statusCode >= 200 && $this->statusCode < 300);
565 }
566
567 568 569 570
571 public function isOk()
572 {
573 return (200 === $this->statusCode);
574 }
575
576 577 578 579
580 public function isNotFound()
581 {
582 return (404 === $this->statusCode);
583 }
584
585 586 587 588
589 public function isForbidden()
590 {
591 return (403 === $this->statusCode);
592 }
593
594 595 596 597
598 public function isRedirect()
599 {
600 return ($this->headers->has('Location') && ($this->statusCode >= 300 && $this->statusCode < 400));
601 }
602
603 604 605 606
607 public function isClientError()
608 {
609 return ($this->statusCode >= 400 && $this->statusCode < 500);
610 }
611
612 613 614 615
616 public function isServerError()
617 {
618 return ($this->statusCode >= 500 && $this->statusCode < 600);
619 }
620
621 622 623 624
625 public function isEmpty()
626 {
627 return in_array($this->statusCode, array(201, 204, 304));
628 }
629
630 631 632 633
634 public function isInvalid()
635 {
636 return ($this->statusCode < 100 || $this->statusCode >= 600);
637 }
638
639 640 641 642
643 public function isInformational()
644 {
645 return ($this->statusCode >= 100 && $this->statusCode < 200);
646 }
647
648 649 650 651 652 653 654 655 656 657
658 protected static function validateContent($content)
659 {
660 if (!is_string($content) && !is_numeric($content) && !is_callable(array($content, '__toString'))) {
661 return false;
662 }
663
664 return true;
665 }
666
667
668
669
670 }