Completed
Push — master ( 322507...c0f65e )
by Neomerx
09:43
created

WhoopsThrowableHandler   A

Complexity

Total Complexity 2

Size/Duplication

Total Lines 156
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 2
dl 0
loc 156
ccs 36
cts 36
cp 1
rs 10
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
B createResponse() 0 41 3
A getSettings() 0 17 3
A logException() 0 15 3
A hp$0 ➔ __construct() 0 5 1
A hp$1 ➔ __construct() 0 5 1
A createThrowableTextResponse() 0 21 1
A createThrowableHtmlResponse() 0 21 1
1
<?php namespace Limoncello\Application\ExceptionHandlers;
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\Application\ApplicationConfigurationInterface as A;
21
use Limoncello\Contracts\Application\CacheSettingsProviderInterface;
22
use Limoncello\Contracts\Exceptions\ThrowableHandlerInterface;
23
use Limoncello\Contracts\Http\ThrowableResponseInterface;
24
use Limoncello\Core\Application\ThrowableResponseTrait;
25
use Psr\Container\ContainerInterface;
26
use Psr\Log\LoggerInterface;
27
use Throwable;
28
use Whoops\Handler\PrettyPageHandler;
29
use Whoops\Run;
30
use Zend\Diactoros\Response\HtmlResponse;
31
use Zend\Diactoros\Response\TextResponse;
32
33
/**
34
 * @package Limoncello\Application
35
 *
36
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
37
 */
38
class WhoopsThrowableHandler implements ThrowableHandlerInterface
39
{
40
    /** Default HTTP code. */
41
    protected const DEFAULT_HTTP_ERROR_CODE = 500;
42
43
    /**
44
     * @inheritdoc
45
     *
46
     * @SuppressWarnings(PHPMD.ElseExpression)
47
     */
48 3
    public function createResponse(Throwable $throwable, ContainerInterface $container): ThrowableResponseInterface
49
    {
50 3
        $message = 'Internal Server Error';
51
52 3
        $this->logException($throwable, $container, $message);
53
54 3
        list($isDebug, $appName, $exceptionDumper) = $this->getSettings($container);
55
56 3
        if ($isDebug === true) {
57 2
            $run = new Run();
58
59
            // If these two options are not used it would work fine with PHP Unit and XDebug,
60
            // however it produces output to console under PhpDbg. So we need a couple of
61
            // tweaks to make it work predictably in both environments.
62
            //
63
            // this one forbids Whoops spilling output to console
64 2
            $run->writeToOutput(false);
65
            // by default after sending error to output Whoops stops execution
66
            // as we want just generated output `string` we instruct not to halt
67 2
            $run->allowQuit(false);
68
69 2
            $handler = new PrettyPageHandler();
70
            // without the line below Whoops is too smart and do not produce any output in tests
71 2
            $handler->handleUnconditionally(true);
72
73 2
            if ($exceptionDumper !== null) {
74 2
                $appSpecificDetails = call_user_func($exceptionDumper, $throwable, $container);
75 2
                $handler->addDataTable("$appName Details", $appSpecificDetails);
76
            }
77
78 2
            $handler->setPageTitle("Whoops! There was a problem with '$appName'.");
79 2
            $run->pushHandler($handler);
0 ignored issues
show
Documentation introduced by
$handler is of type object<Whoops\Handler\PrettyPageHandler>, but the function expects a callable.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
80
81 2
            $html     = $run->handleException($throwable);
82 2
            $response = $this->createThrowableHtmlResponse($throwable, $html, static::DEFAULT_HTTP_ERROR_CODE);
0 ignored issues
show
Security Bug introduced by
It seems like $html defined by $run->handleException($throwable) on line 81 can also be of type false; however, Limoncello\Application\E...ThrowableHtmlResponse() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
83
        } else {
84 1
            $response = $this->createThrowableTextResponse($throwable, $message, static::DEFAULT_HTTP_ERROR_CODE);
85
        }
86
87 3
        return $response;
88
    }
89
90
    /**
91
     * @param ContainerInterface $container
92
     *
93
     * @return array
94
     */
95 3
    private function getSettings(ContainerInterface $container): array
96
    {
97 3
        $appConfig = null;
98
99
        /** @var CacheSettingsProviderInterface $settingsProvider */
100 3
        if ($container->has(CacheSettingsProviderInterface::class) === true &&
101 3
            ($settingsProvider = $container->get(CacheSettingsProviderInterface::class)) !== null
102
        ) {
103 3
            $appConfig = $settingsProvider->getApplicationConfiguration();
104
        }
105
106
        return [
107 3
            $appConfig[A::KEY_IS_DEBUG] ?? false,
108
            $appConfig[A::KEY_APP_NAME] ?? null,
109
            $appConfig[A::KEY_EXCEPTION_DUMPER] ?? null,
110
        ];
111
    }
112
113
    /**
114
     * @param Throwable          $exception
115
     * @param ContainerInterface $container
116
     * @param string             $message
117
     *
118
     * @return void
119
     */
120 3
    private function logException(Throwable $exception, ContainerInterface $container, string $message): void
121
    {
122 3
        if ($container->has(LoggerInterface::class) === true) {
123
            /** @var LoggerInterface $logger */
124 3
            $logger = $container->get(LoggerInterface::class);
125
126
            // The sad truth is that when you have a problem logging might not be available (e.g. no permissions
127
            // to write on a disk). We can't do much with it and can only hope that the error information will be
128
            // delivered to the user other way.
129
            try {
130 3
                $logger->critical($message, ['exception' => $exception]);
131 1
            } catch (Exception $secondException) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
132
            }
133
        }
134
    }
135
136
    /**
137
     * @param Throwable $throwable
138
     * @param string    $text
139
     * @param int       $status
140
     *
141
     * @return ThrowableResponseInterface
142
     */
143 1
    private function createThrowableTextResponse(
144
        Throwable $throwable,
145
        string $text,
146
        int $status
147
    ): ThrowableResponseInterface {
148
        return new class ($throwable, $text, $status) extends TextResponse implements ThrowableResponseInterface
149
        {
150
            use ThrowableResponseTrait;
151
152
            /**
153
             * @param Throwable $throwable
154
             * @param string    $text
155
             * @param int       $status
156
             */
157
            public function __construct(Throwable $throwable, string $text, int $status)
158
            {
159 1
                parent::__construct($text, $status);
160 1
                $this->setThrowable($throwable);
161
            }
162
        };
163
    }
164
165
    /**
166
     * @param Throwable $throwable
167
     * @param string    $text
168
     * @param int       $status
169
     *
170
     * @return ThrowableResponseInterface
171
     */
172
    private function createThrowableHtmlResponse(
173
        Throwable $throwable,
174
        string $text,
175
        int $status
176
    ): ThrowableResponseInterface {
177
        return new class ($throwable, $text, $status) extends HtmlResponse implements ThrowableResponseInterface
178
        {
179
            use ThrowableResponseTrait;
180
181
            /**
182
             * @param Throwable $throwable
183
             * @param string    $text
184
             * @param int       $status
185
             */
186 2
            public function __construct(Throwable $throwable, string $text, int $status)
187
            {
188 2
                parent::__construct($text, $status);
189 2
                $this->setThrowable($throwable);
190
            }
191
        };
192
    }
193
}
194