Completed
Push — master ( 9235b1...2182c9 )
by Nazar
04:49
created

Response   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 207
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 1 Features 1
Metric Value
dl 0
loc 207
ccs 84
cts 84
cp 1
rs 9.8
c 1
b 1
f 1
wmc 31
lcom 1
cbo 5

8 Methods

Rating   Name   Duplication   Size   Complexity  
A init() 0 11 3
A init_with_typical_default_settings() 0 13 1
A header() 0 11 4
A redirect() 0 6 1
D cookie() 0 35 9
A output_default() 0 8 2
A output_default_cli() 0 13 4
B output_default_web() 0 19 7
1
<?php
2
/**
3
 * @package   CleverStyle Framework
4
 * @author    Nazar Mokrynskyi <[email protected]>
5
 * @copyright Copyright (c) 2016, Nazar Mokrynskyi
6
 * @license   MIT License, see license.txt
7
 */
8
namespace cs;
9
use
10
	cs\Response\Psr7;
11
use function
12
	cli\err,
13
	cli\out;
14
15
class Response {
16
	use
17
		Singleton,
18
		Psr7;
19
	/**
20
	 * Protocol, for instance: `HTTP/1.0`, `HTTP/1.1` (default), HTTP/2.0
21
	 *
22
	 * @var string
23
	 */
24
	public $protocol;
25
	/**
26
	 * HTTP status code
27
	 *
28
	 * @var int
29
	 */
30
	public $code;
31
	/**
32
	 * Headers are normalized to lowercase keys with hyphen as separator, for instance: `connection`, `referer`, `content-type`, `accept-language`
33
	 *
34
	 * Values might be strings in case of single value or array of strings in case of multiple values with the same field name
35
	 *
36
	 * @var string[][]
37
	 */
38
	public $headers;
39
	/**
40
	 * String body (is used instead of `$this->body_stream` in most cases, ignored if `$this->body_stream` is present)
41
	 *
42
	 * @var string
43
	 */
44
	public $body;
45
	/**
46
	 * Body in form of stream (might be used instead of `$this->body` in some cases, if present, `$this->body` is ignored)
47
	 *
48
	 * Stream is read/write
49
	 *
50
	 * @var resource
51
	 */
52
	public $body_stream;
53
	/**
54
	 * Initialize response object with specified data
55
	 *
56
	 * @param string               $body
57
	 * @param null|resource|string $body_stream String, like `php://temp` or resource, like `fopen('php://temp', 'a+b')`, if present, `$body` is ignored
58
	 * @param string[]|string[][]  $headers     Headers are normalized to lowercase keys with hyphen as separator, for instance: `connection`, `referer`,
59
	 *                                          `content-type`, `accept-language`; Values might be strings in case of single value or array of strings in case
60
	 *                                          of multiple values with the same field name
61
	 * @param int                  $code        HTTP status code
62
	 * @param string               $protocol    Protocol, for instance: `HTTP/1.0`, `HTTP/1.1` (default), HTTP/2.0
63
	 *
64
	 * @return Response
65
	 */
66 10
	function init ($body = '', $body_stream = null, $headers = [], $code = 200, $protocol = 'HTTP/1.1') {
67 10
		$this->protocol = $protocol;
68 10
		$this->code     = $code;
69 10
		$this->headers  = _array($headers);
70 10
		$this->body     = $body;
71 10
		if ($this->body_stream) {
72 2
			fclose($this->body_stream);
73
		}
74 10
		$this->body_stream = is_string($body_stream) ? fopen($body_stream, 'a+b') : $body_stream;
75 10
		return $this;
76
	}
77
	/**
78
	 * Initialize with typical default settings (headers `Content-Type`, `Vary` and `X-UA-Compatible`, protocol taken from `cs\Request::$protocol`)
79
	 *
80
	 * @return Response
81
	 */
82 10
	function init_with_typical_default_settings () {
83 10
		return $this->init(
84 10
			'',
85 10
			null,
86
			[
87 10
				'Content-Type'    => 'text/html; charset=utf-8',
88
				'Vary'            => 'Accept-Language,User-Agent,Cookie',
89
				'X-UA-Compatible' => 'IE=edge' // TODO: I hope some day we'll get rid of this sh*t :(
90
			],
91 10
			200,
92 10
			Request::instance()->protocol
93
		);
94
	}
95
	/**
96
	 * Set raw HTTP header
97
	 *
98
	 * @param string $field        Field
99
	 * @param string $value        Value, empty string will cause header removal
100
	 * @param bool   $replace      The optional replace parameter indicates whether the header should replace a previous similar header, or add a second header
101
	 *                             of the same type. By default it will replace
102
	 *
103
	 * @return Response
104
	 */
105 58
	function header ($field, $value, $replace = true) {
106 58
		$field = strtolower($field);
107 58
		if ($value === '') {
108 2
			unset($this->headers[$field]);
109 58
		} elseif ($replace || !isset($this->headers[$field])) {
110 58
			$this->headers[$field] = [$value];
111
		} else {
112 18
			$this->headers[$field][] = $value;
113
		}
114 58
		return $this;
115
	}
116
	/**
117
	 * Make redirect to specified location
118
	 *
119
	 * @param string $location
120
	 * @param int    $code
121
	 *
122
	 * @return Response
123
	 */
124 2
	function redirect ($location, $code = 302) {
125 2
		$this->header('location', $location);
126 2
		$this->code                 = $code;
127 2
		Page::instance()->interface = false;
128 2
		return $this;
129
	}
130
	/**
131
	 * Function for setting cookies, taking into account cookies prefix. Parameters like in system `setcookie()` function, but `$path`, `$domain` and `$secure`
132
	 * are skipped, they are detected automatically
133
	 *
134
	 * This function have side effect of setting cookie on `cs\Request` object
135
	 *
136
	 * @param string $name
137
	 * @param string $value
138
	 * @param int    $expire Unix timestamp in seconds
139
	 * @param bool   $httponly
140
	 *
141
	 * @return Response
142
	 */
143 24
	function cookie ($name, $value, $expire = 0, $httponly = false) {
144 24
		$Request = Request::instance();
145 24
		$Config  = Config::instance();
146 24
		$prefix  = '';
147 24
		$domain  = explode(':', $Request->host)[0];
148 24
		if ($Config) {
149 24
			$prefix         = $Config->core['cookie_prefix'];
150 24
			$cookie_domains = $Config->core['cookie_domain'];
151 24
			$domain         = isset($cookie_domains[$Request->mirror_index]) ? $cookie_domains[$Request->mirror_index] : $cookie_domains[0];
152
		}
153 24
		if ($value === '') {
154 12
			unset($Request->cookie[$name], $Request->cookie[$prefix.$name]);
155
		} else {
156 24
			$Request->cookie[$name]         = $value;
157 24
			$Request->cookie[$prefix.$name] = $value;
158
		}
159
		$header = [
160 24
			rawurlencode($prefix.$name).'='.rawurlencode($value),
161 24
			'path=/'
162
		];
163 24
		if ($expire || !$value) {
164 24
			$header[] = 'expires='.gmdate('D, d-M-Y H:i:s', $expire).' GMT';
165
		}
166 24
		if ($domain) {
167 24
			$header[] = "domain=$domain";
168
		}
169 24
		if ($Request->secure) {
170 2
			$header[] = 'secure';
171
		}
172 24
		if ($httponly) {
173 24
			$header[] = 'HttpOnly';
174
		}
175 24
		$this->header('set-cookie', implode('; ', $header), false);
176 24
		return $this;
177
	}
178
	/**
179
	 * Provides default output for all the response data using `header()`, `http_response_code()` and `echo` or `php://output`
180
	 */
181 2
	function output_default () {
182 2
		ob_implicit_flush(true);
183 2
		if (Request::instance()->cli_path) {
184 2
			$this->output_default_cli();
185
		} else {
186 2
			$this->output_default_web();
187
		}
188 2
	}
189 2
	protected function output_default_cli () {
190 2
		if ($this->code >= 400 && $this->code <= 510) {
191 2
			err($this->body);
192 2
			exit($this->code % 256);
193
		}
194 2
		if (is_resource($this->body_stream)) {
195 2
			$position = ftell($this->body_stream);
196 2
			stream_copy_to_stream($this->body_stream, fopen('php://stdout', 'wb'));
197 2
			fseek($this->body_stream, $position);
198
		} else {
199 2
			out($this->body);
200
		}
201 2
	}
202 2
	protected function output_default_web () {
203 2
		foreach ($this->headers ?: [] as $header => $value) {
204 2
			$header = ucwords($header, '-');
205 2
			foreach ($value as $v) {
206 2
				header("$header: $v", false);
207
			}
208
		}
209 2
		http_response_code($this->code);
210 2
		if ($this->code >= 300 && $this->code < 400) {
211 2
			return;
212
		}
213 2
		if (is_resource($this->body_stream)) {
214 2
			$position = ftell($this->body_stream);
215 2
			stream_copy_to_stream($this->body_stream, fopen('php://output', 'wb'));
216 2
			fseek($this->body_stream, $position);
217
		} else {
218 2
			echo $this->body;
219
		}
220 2
	}
221
}
222