Completed
Push — master ( af910b...332a1e )
by
unknown
04:04
created

Rescuer   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 227
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 97.12%

Importance

Changes 0
Metric Value
dl 0
loc 227
ccs 101
cts 104
cp 0.9712
rs 9.6
c 0
b 0
f 0
wmc 32
lcom 1
cbo 7

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 2
A getPriority() 0 4 1
A __invoke() 0 8 2
A getStackTrace() 0 12 2
A renderHtml() 0 4 1
C process() 0 38 12
C getValues() 0 50 11
A renderJson() 0 12 1
1
<?php
2
declare(strict_types=1);
3
/**
4
 * Minotaur
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
7
 * use this file except in compliance with the License. You may obtain a copy of
8
 * 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, WITHOUT
14
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15
 * License for the specific language governing permissions and limitations under
16
 * the License.
17
 *
18
 * @copyright 2015-2017 Appertly
19
 * @license   Apache-2.0
20
 */
21
namespace Minotaur\Http;
22
23
use Psr\Http\Message\ServerRequestInterface as Request;
24
use Psr\Http\Message\ResponseInterface as Response;
25
use Caridea\Http\ProblemDetails;
26
27
/**
28
 * A pretty basic contingency plan.
29
 *
30
 * Under normal circumstances, this class will simply return the response given
31
 * by the `$next` function. In the event that an Exception occurred in the
32
 * `$next` function, this class will craft a new response containing details
33
 * about the error itself.
34
 */
35
class Rescuer implements \Minotaur\Route\Plugin
36
{
37
    /**
38
     * @var bool Whether to include exception information in responses
39
     */
40
    protected $debug;
41
    /**
42
     * @var string The class name of the XHP to render
43
     */
44
    protected $xhpClass;
45
46
    /**
47
     * Convenient map of HTTP status codes to human-readable explanations
48
     */
49
    protected const MESSAGES = [
50
        403 => "You are not allowed to perform this action.",
51
        404 => "We don't have anything at this URL. Double-check the URL you requested.",
52
        405 => "You can't use that HTTP method for this URL. Check the Allow response header for the ones you can.",
53
        406 => "We don't have any content available in the MIME type you specified in your Accept header. Try specifying additional MIME types.",
54
        422 => "There was a problem with the data you submitted. Review the messages for each field and try again.",
55
        423 => "This data is locked. You have permission, but it is no longer allowed to be changed.",
56
        500 => "It looks like we have a problem on our end! Our staff has been notified. Please try again later."
57
    ];
58
59
    /**
60
     * Creates a new Contingency.
61
     *
62
     * The following options are available:
63
     * * `debug` – Whether to include exception stack trace information (*should be `false` in production!*). Defaults to `false`.
64
     * * `xhpClass` – The class name of XHP to render (must be `xhp_class_name` format). Defaults to `xhp__labrys__error_page`.
65
     *
66
     * @param array<string,mixed> $options The options
67
     */
68 10
    public function __construct(array $options = [])
69
    {
70 10
        $this->debug = (bool)($options['debug'] ?? false);
71 10
        $c = (string)($options['xhpClass'] ?? 'labrys_error_page');
72 10
        if (!is_subclass_of($c, \Minotaur\Tags\Primitive::class)) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if \Minotaur\Tags\Primitive::class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
73
            throw new \InvalidArgumentException("Class given in xhpClass option '$c' does not extend \Minotaur\Tags\Node");
74
        }
75 10
        $this->xhpClass = $c;
76 10
    }
77
78
    /**
79
     * Gets the plugin priority; larger means first.
80
     *
81
     * @return - The plugin priority
0 ignored issues
show
Documentation introduced by
The doc-type - could not be parsed: Unknown type name "-" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
82
     */
83
    public function getPriority(): int
84
    {
85
        return PHP_INT_MAX;
86
    }
87
88
    /**
89
     * Middleware request–response handling.
90
     *
91
     * @param $request - The server request
92
     * @param $response - The response
93
     * @param callable $next - The next layer. (function (Request,Response): Response)
94
     * @return - The response
0 ignored issues
show
Documentation introduced by
The doc-type - could not be parsed: Unknown type name "-" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
95
     */
96 10
    public function __invoke(Request $request, Response $response, callable $next): Response
97
    {
98
        try {
99 10
            return $next($request, $response);
100 10
        } catch (\Exception $e) {
101 10
            return $this->process($request, $response, $e);
102
        }
103
    }
104
105
    /**
106
     * Handles an exception.
107
     *
108
     * This is your chance for logging, changing the HTTP status header, and
109
     * rendering some kind of message for the client.
110
     *
111
     * @param $request - The request
112
     * @param $response - The response
113
     * @param $e - The exception to process
114
     * @return - The new response
0 ignored issues
show
Documentation introduced by
The doc-type - could not be parsed: Unknown type name "-" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
115
     */
116 10
    public function process(Request $request, Response $response, \Exception $e) : Response
117
    {
118 10
        if ($e instanceof \Minotaur\Route\Exception\Unroutable) {
119 3
            $response = $response->withStatus($e->getCode(), $e->getMessage());
120 3
            foreach ($e->getHeaders() as $k => $v) {
121 3
                $response = $response->withHeader($k, $v);
122
            }
123 7
        } elseif ($e instanceof \Caridea\Acl\Exception\Forbidden) {
124 1
            $response = $response->withStatus(403, 'Forbidden');
125 6
        } elseif ($e instanceof \Caridea\Dao\Exception\Unretrievable ||
126 5
            $e instanceof \Caridea\Acl\Exception\Unloadable) {
127 1
            $response = $response->withStatus(404, 'Not Found');
128 5
        } elseif ($e instanceof \Caridea\Dao\Exception\Conflicting ||
129 4
            $e instanceof \Caridea\Dao\Exception\Duplicative) {
130 2
            $response = $response->withStatus(409, 'Conflict');
131 3
        } elseif ($e instanceof \Caridea\Validate\Exception\Invalid) {
132 1
            $response = $response->withStatus(422, 'Unprocessable Entity');
133 2
        } elseif ($e instanceof \Caridea\Dao\Exception\Locked) {
134 1
            $response = $response->withStatus(423, 'Locked');
135
        } else {
136 1
            $response = $response->withStatus(500, 'Internal Server Error');
137
        }
138 10
        $values = $this->getValues($request, $e);
139 10
        $types = new \Caridea\Http\AcceptTypes($request->getServerParams());
140 10
        switch ($types->preferred(['application/json', ProblemDetails::MIME_TYPE_JSON, 'text/html'])) {
141
            /* HH_IGNORE_ERROR[4110]: Not sure why hh_client has a problem with this */
142 10
            case ProblemDetails::MIME_TYPE_JSON:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
143
            /* HH_IGNORE_ERROR[4110]: Not sure why hh_client has a problem with this */
144 10
            case 'application/json':
145 8
                $response = $response->withHeader('Content-Type', ProblemDetails::MIME_TYPE_JSON);
146 8
                $response->getBody()->write((string)$this->renderJson($values));
147 8
                break;
148
            default:
149 2
                $response = $response->withHeader('Content-Type', 'text/html');
150 2
                $response->getBody()->write((string)$this->renderHtml($values));
151
        }
152 10
        return $response;
153
    }
154
155
    /**
156
     * Assembles the values from the Request and Exception.
157
     *
158
     * @param $request - The request
159
     * @param $e - The Exception
160
     * @return array<string,mixed> The assembled values
161
     */
162 10
    protected function getValues(Request $request, \Exception $e): array
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
163
    {
164 10
        $values = [];
165 10
        $extra = ['success' => false];
166 10
        if ($this->debug) {
167 10
            $extra['exception'] = $this->getStackTrace($e);
168
        }
169 10
        if ($e instanceof \Minotaur\Route\Exception\Unroutable) {
170 3
            $code = $e->getCode();
171 3
            $values['title'] = $e->getMessage();
172 3
            $values['status'] = $code;
173 3
            $values['detail'] = self::MESSAGES[$code];
174 7
        } elseif ($e instanceof \Caridea\Acl\Exception\Forbidden) {
175 1
            $values['title'] = 'Access Denied';
176 1
            $values['status'] = 403;
177 1
            $values['detail'] = self::MESSAGES[403];
178 6
        } elseif ($e instanceof \Caridea\Dao\Exception\Unretrievable ||
179 5
            $e instanceof \Caridea\Acl\Exception\Unloadable) {
180 1
            $values['title'] = 'Resource Not Found';
181 1
            $values['status'] = 404;
182 1
            $values['detail'] = self::MESSAGES[404];
183 5
        } elseif ($e instanceof \Caridea\Dao\Exception\Duplicative) {
184 1
            $values['title'] = 'Constraint Violation';
185 1
            $values['status'] = 409;
186 1
            $values['detail'] = 'The data you submitted violates unique constraints. Most likely, this is a result of an existing record with similar data. Double-check existing records and try again.';
187 4
        } elseif ($e instanceof \Caridea\Dao\Exception\Conflicting) {
188 1
            $values['title'] = 'Concurrent Modification';
189 1
            $values['status'] = 409;
190 1
            $values['detail'] = 'Someone else saved changes to this same data while you were editing. Try your request again using the latest copy of the record.';
191 3
        } elseif ($e instanceof \Caridea\Validate\Exception\Invalid) {
192 1
            $values['title'] = 'Data Validation Failure';
193 1
            $values['status'] = 422;
194 1
            $values['detail'] = self::MESSAGES[422];
195 1
            $errors = [];
196 1
            foreach ($e->getErrors() as $field => $code) {
197 1
                $errors[] = ['field' => $field, 'code' => $code];
198
            }
199 1
            $extra['errors'] = $errors;
200 2
        } elseif ($e instanceof \Caridea\Dao\Exception\Locked) {
201 1
            $values['title'] = 'Resource Locked';
202 1
            $values['status'] = 423;
203 1
            $values['detail'] = self::MESSAGES[423];
204
        } else {
205 1
            $values['title'] = 'Internal Server Error';
206 1
            $values['status'] = 500;
207 1
            $values['detail'] = self::MESSAGES[500];
208
        }
209 10
        $values['extra'] = $extra;
210 10
        return $values;
211
    }
212
213
    /**
214
     * Gets an exception stack trace as a string, including nested exceptions.
215
     *
216
     * @param $e - The exception
217
     * @return array<string,string> The full stack trace
218
     */
219 10
    private function getStackTrace(\Exception $e): array
220
    {
221
        $details = [
222 10
            'class' => get_class($e),
223 10
            'message' => $e->getMessage(),
224 10
            'stack' => $e->getTraceAsString()
225
        ];
226 10
        if ($e->getPrevious() !== null) {
227 1
            $details['previous'] = $this->getStackTrace($e->getPrevious());
228
        }
229 10
        return $details;
230
    }
231
232
    /**
233
     * Returns the ProblemDetails to render.
234
     *
235
     * @param array<string,mixed> $values The values
236
     * @return ProblemDetails The JSON response
237
     */
238 8
    protected function renderJson(array $values): ProblemDetails
239
    {
240 8
        $extra = $values['extra'] ?? [];
241 8
        return new ProblemDetails(
242 8
            null,
243 8
            (string) $values['title'],
244 8
            (int) $values['status'],
245 8
            (string) $values['detail'],
246 8
            null,
247 8
            $extra
248
        );
249
    }
250
251
    /**
252
     * Returns the HTML to render.
253
     *
254
     * @param array<string,mixed> $values The values
255
     * @return \Minotaur\Tags\Node The HTML response
256
     */
257 2
    protected function renderHtml(array $values): \Minotaur\Tags\Node
258
    {
259 2
        return \Minotaur\Tags\fcomposited($this->xhpClass, ['values' => $values]);
260
    }
261
}
262