Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
50 / 50
100.00% covered (success)
100.00%
13 / 13
CRAP
100.00% covered (success)
100.00%
1 / 1
Response
100.00% covered (success)
100.00%
50 / 50
100.00% covered (success)
100.00%
13 / 13
24
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 getRequest
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getInfo
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getStatusCode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isStatusCode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getStatusReason
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setStatusReason
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setHeader
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 getJson
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 isJson
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getStatus
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLinks
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 parseLinkHeader
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
1<?php declare(strict_types=1);
2/*
3 * This file is part of Aplus Framework HTTP Client Library.
4 *
5 * (c) Natan Felles <natanfelles@gmail.com>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10namespace Framework\HTTP\Client;
11
12use Exception;
13use Framework\HTTP\Cookie;
14use Framework\HTTP\Header;
15use Framework\HTTP\Message;
16use Framework\HTTP\ResponseInterface;
17use InvalidArgumentException;
18use JetBrains\PhpStorm\Pure;
19
20/**
21 * Class Response.
22 *
23 * @package http-client
24 */
25class Response extends Message implements ResponseInterface
26{
27    protected Request $request;
28    protected string $protocol;
29    protected int $statusCode;
30    protected string $statusReason;
31    /**
32     * Response curl info.
33     *
34     * @var array<mixed>
35     */
36    protected array $info = [];
37
38    /**
39     * Response constructor.
40     *
41     * @param Request $request
42     * @param string $protocol
43     * @param int $status
44     * @param string $reason
45     * @param array<string,array<int,string>> $headers
46     * @param string $body
47     * @param array<mixed> $info
48     */
49    public function __construct(
50        Request $request,
51        string $protocol,
52        int $status,
53        string $reason,
54        array $headers,
55        string $body,
56        array $info = []
57    ) {
58        $this->request = $request;
59        $this->setProtocol($protocol);
60        $this->setStatusCode($status);
61        $this->setStatusReason($reason);
62        foreach ($headers as $name => $values) {
63            foreach ($values as $value) {
64                $this->appendHeader($name, $value);
65            }
66        }
67        $this->setBody($body);
68        \ksort($info);
69        $this->info = $info;
70    }
71
72    public function getRequest() : Request
73    {
74        return $this->request;
75    }
76
77    /**
78     * @return array<mixed>
79     */
80    public function getInfo() : array
81    {
82        return $this->info;
83    }
84
85    #[Pure]
86    public function getStatusCode() : int
87    {
88        return parent::getStatusCode();
89    }
90
91    /**
92     * @param int $code
93     *
94     * @throws InvalidArgumentException if status code is invalid
95     *
96     * @return bool
97     */
98    public function isStatusCode(int $code) : bool
99    {
100        return parent::isStatusCode($code);
101    }
102
103    #[Pure]
104    public function getStatusReason() : string
105    {
106        return $this->statusReason;
107    }
108
109    /**
110     * @param string $statusReason
111     *
112     * @return static
113     */
114    protected function setStatusReason(string $statusReason) : static
115    {
116        $this->statusReason = $statusReason;
117        return $this;
118    }
119
120    /**
121     * @param string $name
122     * @param string $value
123     *
124     * @throws Exception if Cookie::setExpires fail
125     *
126     * @return static
127     */
128    protected function setHeader(string $name, string $value) : static
129    {
130        if (\strtolower($name) === 'set-cookie') {
131            $values = \str_contains($value, "\n")
132                ? \explode("\n", $value)
133                : [$value];
134            foreach ($values as $val) {
135                $cookie = Cookie::parse($val);
136                if ($cookie) {
137                    $this->setCookie($cookie);
138                }
139            }
140        }
141        return parent::setHeader($name, $value);
142    }
143
144    /**
145     * Get body as decoded JSON.
146     *
147     * @param bool $assoc
148     * @param int|null $options
149     * @param int<1,max> $depth
150     *
151     * @return array<string,mixed>|false|object
152     */
153    public function getJson(bool $assoc = false, int $options = null, int $depth = 512) : array | object | false
154    {
155        if ($options === null) {
156            $options = \JSON_UNESCAPED_UNICODE | \JSON_UNESCAPED_SLASHES;
157        }
158        $body = \json_decode($this->getBody(), $assoc, $depth, $options);
159        if (\json_last_error() !== \JSON_ERROR_NONE) {
160            return false;
161        }
162        return $body;
163    }
164
165    #[Pure]
166    public function isJson() : bool
167    {
168        return $this->parseContentType() === 'application/json';
169    }
170
171    #[Pure]
172    public function getStatus() : string
173    {
174        return $this->getStatusCode() . ' ' . $this->getStatusReason();
175    }
176
177    /**
178     * Get parsed Link header as array.
179     *
180     * NOTE: To be parsed, links must be in the GitHub REST API format.
181     *
182     * @see https://docs.github.com/en/rest/overview/resources-in-the-rest-api#link-header
183     * @see https://docs.aplus-framework.com/guides/libraries/pagination/index.html#http-header-link
184     * @see https://datatracker.ietf.org/doc/html/rfc5988
185     *
186     * @return array<string,string> Associative array with rel as keys and links
187     * as values
188     */
189    public function getLinks() : array
190    {
191        $link = $this->getHeader(Header::LINK);
192        if ($link) {
193            $link = $this->parseLinkHeader($link);
194        }
195        return (array) $link; // @phpstan-ignore-line
196    }
197
198    /**
199     * @param string $headerLink
200     *
201     * @return array<string,string>
202     */
203    protected function parseLinkHeader(string $headerLink) : array
204    {
205        $links = [];
206        $parts = \explode(',', $headerLink, 10);
207        foreach ($parts as $part) {
208            $section = \explode(';', $part, 10);
209            if (\count($section) !== 2) {
210                continue;
211            }
212            $url = \preg_replace('#<(.*)>#', '$1', $section[0]);
213            $name = \preg_replace('#rel="(.*)"#', '$1', $section[1]);
214            $url = \trim($url);
215            $name = \trim($name);
216            $links[$name] = $url;
217        }
218        return $links;
219    }
220}