Completed
Pull Request — master (#18)
by Randy
01:46
created

JSend   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 423
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 0
Metric Value
wmc 49
lcom 1
cbo 11
dl 0
loc 423
c 0
b 0
f 0
rs 8.48

34 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 2
A safeDecode() 0 9 2
A decode() 0 6 1
A from() 0 6 1
A translate() 0 11 2
A success() 0 4 1
A fail() 0 4 1
A error() 0 4 1
A asArray() 0 4 1
A onSuccess() 0 10 2
A onFail() 0 10 2
A onError() 0 10 2
A into() 0 16 4
A intoSuccess() 0 8 3
A intoFail() 0 8 3
A intoError() 0 8 3
A getDefaultHttpStatusCode() 0 4 1
A hasData() 0 4 1
A getData() 0 4 1
A respond() 0 4 1
A getStatusCode() 0 4 1
A getProtocolVersion() 0 4 1
A withProtocolVersion() 0 4 1
A getHeaders() 0 4 1
A hasHeader() 0 4 1
A getHeader() 0 4 1
A getHeaderLine() 0 4 1
A withHeader() 0 4 1
A withAddedHeader() 0 4 1
A withoutHeader() 0 4 1
A getBody() 0 4 1
A withBody() 0 4 1
A withStatus() 0 4 1
A getReasonPhrase() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like JSend often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use JSend, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Demv\JSend;
4
5
use Dgame\Ensurance\Exception\EnsuranceException;
6
use Psr\Http\Message\ResponseInterface;
7
use Psr\Http\Message\StreamInterface;
8
use function Dgame\Extraction\export;
9
10
/**
11
 * Class JSend
12
 * @package Demv\JSend
13
 */
14
final class JSend implements JSendInterface
15
{
16
    use JSendStatusTrait;
17
    use JSendJsonTrait;
18
19
    /**
20
     * @var array
21
     */
22
    private $decoded = [];
23
    /**
24
     * @var JSendInterface
25
     */
26
    private $jsend;
27
28
    /**
29
     * JSend constructor.
30
     *
31
     * @param Status $status
32
     * @param array  $decoded
33
     */
34
    public function __construct(Status $status, array $decoded)
35
    {
36
        if (!array_key_exists('status', $decoded)) {
37
            $decoded['status'] = (string) $status;
38
        }
39
40
        $this->status  = $status;
41
        $this->decoded = $decoded;
42
    }
43
44
    /**
45
     * @param string $json
46
     *
47
     * @return array
48
     * @throws InvalidJsonException
49
     */
50
    public static function safeDecode(string $json): array
51
    {
52
        $decoded = json_decode($json, true);
53
        if (json_last_error() !== JSON_ERROR_NONE) {
54
            throw new InvalidJsonException(json_last_error_msg());
55
        }
56
57
        return $decoded;
58
    }
59
60
    /**
61
     * @param string $json
62
     *
63
     * @return JSend
64
     * @throws InvalidJsonException
65
     */
66
    public static function decode(string $json): self
67
    {
68
        $decoded = self::safeDecode($json);
69
70
        return self::from($decoded);
71
    }
72
73
    /**
74
     * @param array $decoded
75
     *
76
     * @return JSend
77
     */
78
    public static function from(array $decoded): self
79
    {
80
        ['status' => $status] = export('status')->requireAll()->from($decoded);
0 ignored issues
show
Bug introduced by
The variable $status does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
81
82
        return new self(Status::from($status), $decoded);
83
    }
84
85
    /**
86
     * @param ResponseInterface $response
87
     *
88
     * @return JSendInterface
89
     * @throws EnsuranceException
90
     * @throws InvalidJsonException
91
     */
92
    public static function translate(ResponseInterface $response): JSendInterface
93
    {
94
        $code    = $response->getStatusCode();
95
        $body    = $response->getBody();
96
        $decoded = self::safeDecode($body);
97
        if (!array_key_exists('status', $decoded)) {
98
            $decoded['status'] = Status::fromStatusCode($code);
99
        }
100
101
        return self::decode($body)->withStatus($code);
102
    }
103
104
    /**
105
     * @param array|null $data
106
     *
107
     * @return JSend
108
     */
109
    public static function success(array $data = null): self
110
    {
111
        return new self(Status::success(), ['data' => $data]);
112
    }
113
114
    /**
115
     * @param array|null $data
116
     *
117
     * @return JSend
118
     */
119
    public static function fail(array $data = null): self
120
    {
121
        return new self(Status::fail(), ['data' => $data]);
122
    }
123
124
    /**
125
     * @param string   $message
126
     * @param int|null $code
127
     *
128
     * @return JSend
129
     */
130
    public static function error(string $message, int $code = null): self
131
    {
132
        return new self(Status::error(), ['message' => $message, 'code' => $code]);
133
    }
134
135
    /**
136
     * @return array
137
     */
138
    public function asArray(): array
139
    {
140
        return $this->decoded;
141
    }
142
143
    /**
144
     * @param callable $closure
145
     *
146
     * @return bool
147
     */
148
    public function onSuccess(callable $closure): bool
149
    {
150
        if ($this->isSuccess()) {
151
            $closure($this->intoSuccess());
152
153
            return true;
154
        }
155
156
        return false;
157
    }
158
159
    /**
160
     * @param callable $closure
161
     *
162
     * @return bool
163
     */
164
    public function onFail(callable $closure): bool
165
    {
166
        if ($this->isFail()) {
167
            $closure($this->intoFail());
168
169
            return true;
170
        }
171
172
        return false;
173
    }
174
175
    /**
176
     * @param callable $closure
177
     *
178
     * @return bool
179
     */
180
    public function onError(callable $closure): bool
181
    {
182
        if ($this->isError()) {
183
            $closure($this->intoError());
184
185
            return true;
186
        }
187
188
        return false;
189
    }
190
191
    /**
192
     * @return JSendInterface
193
     * @throws EnsuranceException
194
     */
195
    public function into(): JSendInterface
196
    {
197
        if ($this->isSuccess()) {
198
            return $this->intoSuccess();
199
        }
200
201
        if ($this->isFail()) {
202
            return $this->intoFail();
203
        }
204
205
        if ($this->isError()) {
206
            return $this->intoError();
207
        }
208
209
        throw new EnsuranceException('Neither success, fail or error');
210
    }
211
212
    /**
213
     * @return JSendSuccess
214
     */
215
    public function intoSuccess(): JSendSuccess
216
    {
217
        if ($this->jsend === null || !$this->jsend->isSuccess()) {
218
            $this->jsend = JSendSuccess::from($this->decoded);
219
        }
220
221
        return $this->jsend;
222
    }
223
224
    /**
225
     * @return JSendFail
226
     */
227
    public function intoFail(): JSendFail
228
    {
229
        if ($this->jsend === null || !$this->jsend->isFail()) {
230
            $this->jsend = JSendFail::from($this->decoded);
231
        }
232
233
        return $this->jsend;
234
    }
235
236
    /**
237
     * @return JSendError
238
     */
239
    public function intoError(): JSendError
240
    {
241
        if ($this->jsend === null || !$this->jsend->isError()) {
242
            $this->jsend = JSendError::from($this->decoded);
243
        }
244
245
        return $this->jsend;
246
    }
247
248
    /**
249
     * @return int
250
     * @throws EnsuranceException
251
     */
252
    public function getDefaultHttpStatusCode(): int
253
    {
254
        return $this->into()->getDefaultHttpStatusCode();
255
    }
256
257
    /**
258
     * @param string|null $key
259
     *
260
     * @return bool
261
     * @throws EnsuranceException
262
     */
263
    public function hasData(string $key = null): bool
264
    {
265
        return $this->into()->hasData($key);
266
    }
267
268
    /**
269
     * @param string|null $key
270
     * @param null        $default
271
     *
272
     * @return mixed
273
     * @throws EnsuranceException
274
     */
275
    public function getData(string $key = null, $default = null)
276
    {
277
        return $this->into()->getData($key, $default);
278
    }
279
280
    /**
281
     * @param int|null $code
282
     *
283
     * @throws EnsuranceException
284
     */
285
    public function respond(int $code = null): void
286
    {
287
        $this->into()->respond($code);
288
    }
289
290
    /**
291
     * @return int
292
     * @throws EnsuranceException
293
     */
294
    public function getStatusCode(): int
295
    {
296
        return $this->into()->getStatusCode();
297
    }
298
299
    /**
300
     * @return string
301
     * @throws EnsuranceException
302
     */
303
    public function getProtocolVersion()
304
    {
305
        return $this->into()->getProtocolVersion();
306
    }
307
308
    /**
309
     * @param string $version
310
     *
311
     * @return JSendInterface
312
     * @throws EnsuranceException
313
     */
314
    public function withProtocolVersion($version)
315
    {
316
        return $this->into()->withProtocolVersion($version);
317
    }
318
319
    /**
320
     * @return \string[][]
0 ignored issues
show
Documentation introduced by
Should the return type not be string[][]?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
321
     * @throws EnsuranceException
322
     */
323
    public function getHeaders()
324
    {
325
        return $this->into()->getHeaders();
326
    }
327
328
    /**
329
     * @param string $name
330
     *
331
     * @return bool
332
     * @throws EnsuranceException
333
     */
334
    public function hasHeader($name)
335
    {
336
        return $this->into()->hasHeader($name);
337
    }
338
339
    /**
340
     * @param string $name
341
     *
342
     * @return string[]
343
     * @throws EnsuranceException
344
     */
345
    public function getHeader($name)
346
    {
347
        return $this->into()->getHeader($name);
348
    }
349
350
    /**
351
     * @param string $name
352
     *
353
     * @return string
354
     * @throws EnsuranceException
355
     */
356
    public function getHeaderLine($name)
357
    {
358
        return $this->into()->getHeaderLine($name);
359
    }
360
361
    /**
362
     * @param string          $name
363
     * @param string|string[] $value
364
     *
365
     * @return JSendInterface
366
     * @throws EnsuranceException
367
     */
368
    public function withHeader($name, $value)
369
    {
370
        return $this->into()->withHeader($name, $value);
371
    }
372
373
    /**
374
     * @param string          $name
375
     * @param string|string[] $value
376
     *
377
     * @return JSendInterface
378
     * @throws EnsuranceException
379
     */
380
    public function withAddedHeader($name, $value)
381
    {
382
        return $this->into()->withAddedHeader($name, $value);
383
    }
384
385
    /**
386
     * @param string $name
387
     *
388
     * @return JSendInterface
389
     * @throws EnsuranceException
390
     */
391
    public function withoutHeader($name)
392
    {
393
        return $this->into()->withoutHeader($name);
394
    }
395
396
    /**
397
     * @return StreamInterface
0 ignored issues
show
Documentation introduced by
Should the return type not be \Guzzle\Http\EntityBodyInterface?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
398
     * @throws EnsuranceException
399
     */
400
    public function getBody()
401
    {
402
        return $this->into()->getBody();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->into()->getBody(); (Guzzle\Http\EntityBodyInterface) is incompatible with the return type declared by the interface Psr\Http\Message\MessageInterface::getBody of type Psr\Http\Message\StreamInterface.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
403
    }
404
405
    /**
406
     * @param StreamInterface $body
407
     *
408
     * @return JSendInterface
409
     * @throws EnsuranceException
410
     */
411
    public function withBody(StreamInterface $body)
412
    {
413
        return $this->into()->withBody($body);
414
    }
415
416
    /**
417
     * @param int    $code
418
     * @param string $reasonPhrase
419
     *
420
     * @return JSendInterface
421
     * @throws EnsuranceException
422
     */
423
    public function withStatus($code, $reasonPhrase = '')
424
    {
425
        return $this->into()->withStatus($code, $reasonPhrase);
426
    }
427
428
    /**
429
     * @return string
430
     * @throws EnsuranceException
431
     */
432
    public function getReasonPhrase()
433
    {
434
        return $this->into()->getReasonPhrase();
435
    }
436
}
437