Passed
Push — master ( d1cff6...f85f21 )
by Anton
03:09
created

ErrorHandlerMiddleware::logError()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 10
rs 10
c 0
b 0
f 0
cc 2
nc 1
nop 3
1
<?php
2
3
/**
4
 * Spiral Framework.
5
 *
6
 * @license   MIT
7
 * @author    Anton Titov (Wolfy-J)
8
 */
9
10
declare(strict_types=1);
11
12
namespace Spiral\Http\Middleware;
13
14
use Psr\Container\ContainerExceptionInterface;
15
use Psr\Container\ContainerInterface;
16
use Psr\Http\Message\ResponseFactoryInterface;
17
use Psr\Http\Message\ResponseInterface as Response;
18
use Psr\Http\Message\ServerRequestInterface as Request;
19
use Psr\Http\Server\MiddlewareInterface;
20
use Psr\Http\Server\RequestHandlerInterface as Handler;
21
use Spiral\Debug\StateInterface;
22
use Spiral\Exceptions\HtmlHandler;
23
use Spiral\Exceptions\JsonHandler;
24
use Spiral\Http\ErrorHandler\RendererInterface;
25
use Spiral\Http\Exception\ClientException;
26
use Spiral\Logger\Traits\LoggerTrait;
27
use Spiral\Router\Exception\RouterException;
28
use Spiral\Snapshots\SnapshotterInterface;
29
30
/**
31
 * Wraps Client and Routing exceptions into proper response.
32
 */
33
final class ErrorHandlerMiddleware implements MiddlewareInterface
34
{
35
    use LoggerTrait;
36
37
    /** @var ResponseFactoryInterface */
38
    private $responseFactory;
39
40
    /** @var bool */
41
    private $suppressErrors;
42
43
    /** @var RendererInterface */
44
    private $renderer;
45
46
    /** @var ContainerInterface @internal */
47
    private $container;
48
49
    /**
50
     * @param bool                     $suppressErrors
51
     * @param RendererInterface        $renderer
52
     * @param ResponseFactoryInterface $responseFactory
53
     * @param ContainerInterface       $container
54
     */
55
    public function __construct(
56
        bool $suppressErrors,
57
        RendererInterface $renderer,
58
        ResponseFactoryInterface $responseFactory,
59
        ContainerInterface $container
60
    ) {
61
        $this->suppressErrors = $suppressErrors;
62
        $this->renderer = $renderer;
63
        $this->responseFactory = $responseFactory;
64
        $this->container = $container;
65
    }
66
67
    /**
68
     * @inheritdoc
69
     *
70
     * @throws \Throwable
71
     */
72
    public function process(Request $request, Handler $handler): Response
73
    {
74
        try {
75
            return $handler->handle($request);
76
        } catch (ClientException | RouterException $e) {
77
            if ($e instanceof ClientException) {
78
                $code = $e->getCode();
79
            } else {
80
                $code = 404;
81
            }
82
        } catch (\Throwable $e) {
83
            $snapshotter = $this->getOptional(SnapshotterInterface::class);
84
            if ($snapshotter !== null) {
85
                /** @var SnapshotterInterface $snapshotter */
86
                $snapshotter->register($e);
87
            }
88
89
            if ($e instanceof ClientException) {
90
                $code = $e->getCode();
91
            } else {
92
                $code = 500;
93
            }
94
95
            if (!$this->suppressErrors) {
96
                return $this->renderException($request, $e);
97
            }
98
        }
99
100
        $this->logError($request, $code, $e->getMessage());
101
102
        return $this->renderer->renderException($request, $code, $e->getMessage());
103
    }
104
105
    /**
106
     * @param Request    $request
107
     * @param \Throwable $e
108
     * @return Response
109
     *
110
     * @throws \Throwable
111
     */
112
    private function renderException(Request $request, \Throwable $e): Response
113
    {
114
        $response = $this->responseFactory->createResponse(500);
115
116
        if ($request->getHeaderLine('Accept') == 'application/json') {
117
            $response = $response->withHeader('Content-Type', 'application/json');
118
            $handler = new JsonHandler();
119
            $response->getBody()->write(json_encode(
120
                ['status' => 500]
121
                + json_decode(
122
                    $handler->renderException($e, HtmlHandler::VERBOSITY_VERBOSE),
123
                    true
124
                )
125
            ));
126
        } else {
127
            $handler = new HtmlHandler();
128
            $state = $this->getOptional(StateInterface::class);
129
            if ($state !== null) {
130
                $handler = $handler->withState($state);
131
            }
132
133
            $response->getBody()->write($handler->renderException($e, HtmlHandler::VERBOSITY_VERBOSE));
134
        }
135
136
        return $response;
137
    }
138
139
    /**
140
     * @param Request $request
141
     * @param int     $code
142
     * @param string  $message
143
     */
144
    private function logError(Request $request, int $code, string $message): void
145
    {
146
        $this->getLogger()->error(sprintf(
147
            '%s://%s%s caused the error %s (%s) by client %s.',
148
            $request->getUri()->getScheme(),
149
            $request->getUri()->getHost(),
150
            $request->getUri()->getPath(),
151
            $code,
152
            $message ?: '-not specified-',
153
            $request->getServerParams()['REMOTE_ADDR'] ?? '127.0.0.1'
154
        ));
155
    }
156
157
    /**
158
     * @param string $class
159
     * @return mixed|null
160
     */
161
    private function getOptional(string $class)
162
    {
163
        try {
164
            return $this->container->get($class);
165
        } catch (\Throwable | ContainerExceptionInterface $se) {
166
            return null;
167
        }
168
    }
169
}
170