Completed
Push — master ( b6f561...db9bd8 )
by Neomerx
03:16
created

FluteThrowableHandler   A

Complexity

Total Complexity 3

Size/Duplication

Total Lines 184
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 3
dl 0
loc 184
ccs 47
cts 47
cp 1
rs 10
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A hp$0 ➔ __construct() 0 5 1
A hp$0 ➔ getThrowable() 0 4 1
A __construct() 0 18 2
C createResponse() 0 44 7
A logError() 0 10 4
A getHttpCodeForUnexpectedThrowable() 0 4 1
A shouldBeLogged() 0 6 1
B createThrowableJsonApiResponse() 0 32 1
1
<?php namespace Limoncello\Flute\Http\ThrowableHandlers;
2
3
/**
4
 * Copyright 2015-2017 [email protected]
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 * http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
use Exception;
20
use Limoncello\Contracts\Exceptions\ThrowableHandlerInterface;
21
use Limoncello\Contracts\Http\ThrowableResponseInterface;
22
use Limoncello\Flute\Contracts\Encoder\EncoderInterface;
23
use Limoncello\Flute\Contracts\Exceptions\JsonApiThrowableConverterInterface as ConverterInterface;
24
use Limoncello\Flute\Http\JsonApiResponse;
25
use Neomerx\JsonApi\Document\Error;
26
use Neomerx\JsonApi\Exceptions\ErrorCollection;
27
use Neomerx\JsonApi\Exceptions\JsonApiException;
28
use Psr\Container\ContainerInterface;
29
use Psr\Log\LoggerAwareTrait;
30
use Throwable;
31
32
/**
33
 * @package Limoncello\Flute
34
 *
35
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
36
 */
37
class FluteThrowableHandler implements ThrowableHandlerInterface
38
{
39
    use LoggerAwareTrait;
40
41
    /**
42
     * Those classes will not be logged. Note that classes are expected to be keys but not values.
43
     *
44
     * @var array
45
     */
46
    private $doNotLogClassesAsKeys;
47
48
    /**
49
     * @var int
50
     */
51
    private $httpCodeForUnexpected;
52
53
    /**
54
     * @var bool
55
     */
56
    private $isDebug;
57
58
    /**
59
     * @var EncoderInterface
60
     */
61
    private $encoder;
62
63
    /**
64
     * @var string|null
65
     */
66
    private $throwableConverter;
67
68
    /**
69
     * @param EncoderInterface $encoder
70
     * @param array            $noLogClassesAsKeys
71
     * @param int              $codeForUnexpected
72
     * @param bool             $isDebug
73
     * @param null|string      $converterClass
74
     */
75 6
    public function __construct(
76
        EncoderInterface $encoder,
77
        array $noLogClassesAsKeys,
78
        int $codeForUnexpected,
79
        bool $isDebug,
80
        ?string $converterClass
81
    ) {
82 6
        assert(
83 6
            $converterClass === null ||
84 6
            array_key_exists(ConverterInterface::class, class_implements($converterClass)) === true
85
        );
86
87 6
        $this->doNotLogClassesAsKeys = $noLogClassesAsKeys;
88 6
        $this->httpCodeForUnexpected = $codeForUnexpected;
89 6
        $this->isDebug               = $isDebug;
90 6
        $this->encoder               = $encoder;
91 6
        $this->throwableConverter    = $converterClass;
92
    }
93
94
    /**
95
     * @inheritdoc
96
     *
97
     * @SuppressWarnings(PHPMD.ElseExpression)
98
     */
99 5
    public function createResponse(Throwable $throwable, ContainerInterface $container): ThrowableResponseInterface
100
    {
101 5
        unset($container);
102
103 5
        $message            = 'Internal Server Error';
104 5
        $isJsonApiException = $throwable instanceof JsonApiException;
105
106 5
        $this->logError($throwable, $message);
107
108
        // if exception converter is specified it will be used to convert throwable to JsonApiException
109 5
        if ($isJsonApiException === false && $this->throwableConverter !== null) {
110
            try {
111
                /** @var ConverterInterface $converterClass */
112 2
                $converterClass = $this->throwableConverter;
113 2
                if (($converted = $converterClass::convert($throwable)) !== null) {
114 1
                    assert($converted instanceof JsonApiException);
115 1
                    $throwable          = $converted;
116 1
                    $isJsonApiException = true;
117
                }
118 1
            } catch (Throwable $ignored) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
119
            }
120
        }
121
122
        // compose JSON API Error with appropriate level of details
123 5
        if ($isJsonApiException === true) {
124
            /** @var JsonApiException $throwable */
125 2
            $errors   = $throwable->getErrors();
126 2
            $httpCode = $throwable->getHttpCode();
127
        } else {
128 3
            $errors   = new ErrorCollection();
129 3
            $httpCode = $this->getHttpCodeForUnexpectedThrowable();
130 3
            $details  = null;
131 3
            if ($this->isDebug === true) {
132 3
                $message = $throwable->getMessage();
133 3
                $details = (string)$throwable;
134
            }
135 3
            $errors->add(new Error(null, null, $httpCode, null, $message, $details));
136
        }
137
138
        // encode the error and send to client
139 5
        $content = $this->encoder->encodeErrors($errors);
140
141 5
        return $this->createThrowableJsonApiResponse($throwable, $content, $httpCode);
142
    }
143
144
    /**
145
     * @param Throwable $throwable
146
     * @param string    $message
147
     *
148
     * @return void
149
     */
150 5
    private function logError(Throwable $throwable, string $message): void
151
    {
152 5
        if ($this->logger !== null && $this->shouldBeLogged($throwable) === true) {
153
            // on error (e.g. no permission to write on disk or etc) ignore
154
            try {
155 5
                $this->logger->error($message, ['error' => $throwable]);
156 1
            } catch (Exception $exception) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
157
            }
158
        }
159
    }
160
161
    /**
162
     * @return int
163
     */
164 3
    private function getHttpCodeForUnexpectedThrowable(): int
165
    {
166 3
        return $this->httpCodeForUnexpected;
167
    }
168
169
    /**
170
     * @param Throwable $throwable
171
     *
172
     * @return bool
173
     */
174 5
    private function shouldBeLogged(Throwable $throwable): bool
175
    {
176 5
        $result = array_key_exists(get_class($throwable), $this->doNotLogClassesAsKeys) === false;
177
178 5
        return $result;
179
    }
180
181
    /**
182
     * @param Throwable $throwable
183
     * @param string    $content
184
     * @param int       $status
185
     *
186
     * @return ThrowableResponseInterface
187
     */
188
    private function createThrowableJsonApiResponse(
189
        Throwable $throwable,
190
        string $content,
191
        int $status
192
    ): ThrowableResponseInterface {
193
        return new class ($throwable, $content, $status) extends JsonApiResponse implements ThrowableResponseInterface
194
        {
195
            /**
196
             * @var Throwable
197
             */
198
            private $throwable;
199
200
            /**
201
             * @param Throwable $throwable
202
             * @param string    $content
203
             * @param int       $status
204
             */
205 5
            public function __construct(Throwable $throwable, string $content, int $status)
206
            {
207 5
                parent::__construct($content, $status);
208 5
                $this->throwable = $throwable;
209
            }
210
211
            /**
212
             * @return Throwable
213
             */
214 5
            public function getThrowable(): Throwable
215
            {
216 5
                return $this->throwable;
217
            }
218
        };
219
    }
220
}
221