1 <?php
2 /**
3 * Copyright 2019 Klarna AB
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17 * File containing the abstract base resource class.
18 */
19
20 namespace Klarna\Rest;
21
22 use GuzzleHttp\Exception\RequestException;
23 use Klarna\Rest\Transport\ConnectorInterface;
24 use Klarna\Rest\Transport\Method;
25 use Klarna\Rest\Transport\Exception\ConnectorException;
26 use Klarna\Rest\Transport\ResponseValidator;
27
28 /**
29 * Abstract resource class.
30 */
31 abstract class Resource extends \ArrayObject
32 {
33 /**
34 * Id property field name.
35 */
36 const ID_FIELD = 'id';
37
38 /**
39 * Path to the resource endpoint.
40 *
41 * @var string
42 */
43 public static $path;
44
45 /**
46 * HTTP transport connector instance.
47 *
48 * @var Connector
49 */
50 protected $connector;
51
52 /**
53 * Url to the resource.
54 *
55 * @var string
56 */
57 protected $url;
58
59 /**
60 * Constructs a resource instance.
61 *
62 * @param ConnectorInterface $connector HTTP transport instance.
63 */
64 public function __construct(ConnectorInterface $connector)
65 {
66 $this->connector = $connector;
67 }
68
69 /**
70 * Gets the resource id.
71 *
72 * @return string|null
73 */
74 public function getId()
75 {
76 return isset($this[static::ID_FIELD]) ? $this[static::ID_FIELD] : null;
77 }
78
79 /**
80 * Gets the resource location.
81 *
82 * @return string|null
83 */
84 public function getLocation()
85 {
86 return $this->url;
87 }
88
89 /**
90 * Sets the resource location.
91 *
92 * @param string $url Url to the resource
93 *
94 * @return self
95 */
96 public function setLocation($url)
97 {
98 $this->url = $url;
99
100 return $this;
101 }
102
103 /**
104 * Overrides: Stores the ID KEY field in order to restore it after exchanging the array without
105 * the ID field.
106 *
107 * @param array $array Data to be exchanged
108 */
109 public function exchangeArray($array)
110 {
111 $id = $this->getId();
112
113 if (!is_null($array)) {
114 parent::exchangeArray($array);
115 }
116 if (is_null($this->getId()) && !is_null($id)) {
117 $this->setId($id);
118 }
119 }
120
121 /**
122 * Fetches the resource.
123 *
124 * @throws ConnectorException When the API replies with an error response
125 * @throws RequestException When an error is encountered
126 * @throws \RuntimeException On an unexpected API response
127 * @throws \RuntimeException If the response content type is not JSON
128 * @throws \InvalidArgumentException If the JSON cannot be parsed
129 * @throws \LogicException When Guzzle cannot populate the response
130 *
131 * @return self
132 */
133 public function fetch()
134 {
135 $data = $this->get($this->getLocation())
136 ->expectSuccessfull()
137 ->status('200')
138 ->contentType('application/json')
139 ->getJson();
140
141 $this->exchangeArray($data);
142
143 return $this;
144 }
145
146 /**
147 * Sets new ID KEY field.
148 *
149 * @param mixed $id ID field
150 *
151 * @return self
152 */
153 protected function setId($id)
154 {
155 $this[static::ID_FIELD] = $id;
156 return $this;
157 }
158
159 /**
160 * Sends a HTTP request to the specified url.
161 *
162 * @param string $method HTTP method, e.g. 'GET'
163 * @param string $url Request destination
164 * @param array $headers
165 * @param string $body
166 *
167 * @throws ConnectorException When the API replies with an error response
168 * @throws RequestException When an error is encountered
169 * @throws \LogicException When Guzzle cannot populate the response'
170 * @return ResponseValidator When the API replies with an error response
171 *
172 */
173 protected function request($method, $url, array $headers = [], $body = null)
174 {
175 $debug = getenv('DEBUG_SDK') || defined('DEBUG_SDK');
176
177 if ($debug) {
178 $methodDebug = str_pad($method, 7, ' ', STR_PAD_LEFT);
179 $debugHeaders = json_encode($headers);
180 echo <<<DEBUG_BODY
181 DEBUG MODE: Request
182 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
183 {$methodDebug} : {$url}
184 Headers : $debugHeaders
185 Body : {$body}
186 \n
187 DEBUG_BODY;
188 }
189
190 switch ($method) {
191 case Method::GET:
192 $response = $this->connector->get($url, $headers);
193 break;
194 case Method::POST:
195 $response = $this->connector->post($url, $body, $headers);
196 break;
197 case Method::PUT:
198 $response = $this->connector->put($url, $body, $headers);
199 break;
200 case Method::DELETE:
201 $response = $this->connector->delete($url, $body, $headers);
202 break;
203 case Method::PATCH:
204 $response = $this->connector->patch($url, $body, $headers);
205 break;
206 default:
207 throw new \RuntimeException('Unknown request method ' + $method);
208 }
209
210 if ($debug) {
211 $debugHeaders = json_encode($response->getHeaders());
212 echo <<<DEBUG_BODY
213 DEBUG MODE: Response
214 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
215 Headers : $debugHeaders
216 Body : {$response->getBody()}
217 \n
218 DEBUG_BODY;
219 }
220
221 $location = $response->getLocation();
222 if (!empty($location)) {
223 $this->setLocation($location);
224 }
225
226 return new ResponseValidator($response);
227 }
228
229 /**
230 * Sends a HTTP GET request to the specified url.
231 *
232 * @param string $url Request destination
233 *
234 * @throws ConnectorException When the API replies with an error response
235 * @throws RequestException When an error is encountered
236 * @throws \LogicException When Guzzle cannot populate the response
237 *
238 * @return ResponseValidator
239 */
240 protected function get($url)
241 {
242 return $this->request('GET', $url);
243 }
244
245 /**
246 * Sends a HTTP DELETE request to the specified url.
247 *
248 * @param string $url Request destination
249 * @param array $data Data to be JSON encoded
250 *
251 * @throws ConnectorException When the API replies with an error response
252 * @throws RequestException When an error is encountered
253 * @throws \LogicException When Guzzle cannot populate the response
254 *
255 * @return ResponseValidator
256 */
257 protected function delete($url, array $data = null)
258 {
259 return $this->request(
260 'DELETE',
261 $url,
262 ['Content-Type' => 'application/json'],
263 $data !== null ? json_encode($data) : null
264 );
265 }
266
267 /**
268 * Sends a HTTP PATCH request to the specified url.
269 *
270 * @param string $url Request destination
271 * @param array $data Data to be JSON encoded
272 *
273 * @throws ConnectorException When the API replies with an error response
274 * @throws RequestException When an error is encountered
275 * @throws \LogicException When Guzzle cannot populate the response
276 *
277 * @return ResponseValidator
278 */
279 protected function patch($url, array $data)
280 {
281 return $this->request(
282 'PATCH',
283 $url,
284 ['Content-Type' => 'application/json'],
285 json_encode($data)
286 );
287 }
288
289 /**
290 * Sends a HTTP PUT request to the specified url.
291 *
292 * @param string $url Request destination
293 * @param array $data Data to be JSON encoded
294 *
295 * @throws ConnectorException When the API replies with an error response
296 * @throws RequestException When an error is encountered
297 * @throws \LogicException When Guzzle cannot populate the response
298 *
299 * @return ResponseValidator
300 */
301 protected function put($url, array $data)
302 {
303 return $this->request(
304 'PUT',
305 $url,
306 ['Content-Type' => 'application/json'],
307 json_encode($data)
308 );
309 }
310
311 /**
312 * Sends a HTTP POST request to the specified url.
313 *
314 * @param string $url Request destination
315 * @param array $data Data to be JSON encoded
316 *
317 * @throws ConnectorException When the API replies with an error response
318 * @throws RequestException When an error is encountered
319 * @throws \LogicException When Guzzle cannot populate the response
320 *
321 * @return ResponseValidator
322 */
323 protected function post($url, array $data = null)
324 {
325 return $this->request(
326 'POST',
327 $url,
328 ['Content-Type' => 'application/json'],
329 $data !== null ? \json_encode($data) : null
330 );
331 }
332 }
333