Completed
Push — master ( 79dcd5...ac1fa0 )
by Neomerx
02:10
created

er.php$0   A

Complexity

Total Complexity 1

Size/Duplication

Total Lines 15
Duplicated Lines 0 %

Coupling/Cohesion

Components 0
Dependencies 2

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 1
lcom 0
cbo 2
dl 0
loc 15
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
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\ContainerExceptionInterface;
26
use Psr\Container\ContainerInterface;
27
use Psr\Container\NotFoundExceptionInterface;
28
use Psr\Log\LoggerInterface;
29
use Throwable;
30
use Whoops\Handler\PrettyPageHandler;
31
use Whoops\Run;
32
use Zend\Diactoros\Response\HtmlResponse;
33
use Zend\Diactoros\Response\TextResponse;
34
35
/**
36
 * @package Limoncello\Application
37
 *
38
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
39
 */
40
class WhoopsThrowableHtmlHandler implements ThrowableHandlerInterface
41
{
42
    /** Default HTTP code. */
43
    protected const DEFAULT_HTTP_ERROR_CODE = 500;
44
45
    /**
46
     * @inheritdoc
47
     *
48
     * @SuppressWarnings(PHPMD.ElseExpression)
49
     */
50 3
    public function createResponse(Throwable $throwable, ContainerInterface $container): ThrowableResponseInterface
51
    {
52 3
        $message = 'Internal Server Error';
53
54 3
        $this->logException($throwable, $container, $message);
55
56 3
        list($isDebug, $appName, $exceptionDumper) = $this->getSettings($container);
57
58 3
        if ($isDebug === true) {
59 2
            $run = new Run();
60
61
            // If these two options are not used it would work fine with PHP Unit and XDebug,
62
            // however it produces output to console under PhpDbg. So we need a couple of
63
            // tweaks to make it work predictably in both environments.
64
            //
65
            // this one forbids Whoops spilling output to console
66 2
            $run->writeToOutput(false);
67
            // by default after sending error to output Whoops stops execution
68
            // as we want just generated output `string` we instruct not to halt
69 2
            $run->allowQuit(false);
70
71 2
            $handler = new PrettyPageHandler();
72
            // without the line below Whoops is too smart and do not produce any output in tests
73 2
            $handler->handleUnconditionally(true);
74
75 2
            if ($exceptionDumper !== null) {
76 2
                $appSpecificDetails = call_user_func($exceptionDumper, $throwable, $container);
77 2
                $handler->addDataTable("$appName Details", $appSpecificDetails);
78
            }
79
80 2
            $handler->setPageTitle("Whoops! There was a problem with '$appName'.");
81 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...
82
83 2
            $html     = $run->handleException($throwable);
84 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 83 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...
85
        } else {
86 1
            $response = $this->createThrowableTextResponse($throwable, $message, static::DEFAULT_HTTP_ERROR_CODE);
87
        }
88
89 3
        return $response;
90
    }
91
92
    /**
93
     * @param ContainerInterface $container
94
     *
95
     * @return array
96
     *
97
     * @throws ContainerExceptionInterface
98
     * @throws NotFoundExceptionInterface
99
     */
100 3
    private function getSettings(ContainerInterface $container): array
101
    {
102 3
        $appConfig = null;
103
104
        /** @var CacheSettingsProviderInterface $settingsProvider */
105 3
        if ($container->has(CacheSettingsProviderInterface::class) === true &&
106 3
            ($settingsProvider = $container->get(CacheSettingsProviderInterface::class)) !== null
107
        ) {
108 3
            $appConfig = $settingsProvider->getApplicationConfiguration();
109
        }
110
111
        return [
112 3
            $appConfig[A::KEY_IS_DEBUG] ?? false,
113
            $appConfig[A::KEY_APP_NAME] ?? null,
114
            $appConfig[A::KEY_EXCEPTION_DUMPER] ?? null,
115
        ];
116
    }
117
118
    /**
119
     * @param Throwable          $exception
120
     * @param ContainerInterface $container
121
     * @param string             $message
122
     *
123
     * @return void
124
     *
125
     * @throws ContainerExceptionInterface
126
     * @throws NotFoundExceptionInterface
127
     */
128 3
    private function logException(Throwable $exception, ContainerInterface $container, string $message): void
129
    {
130 3
        if ($container->has(LoggerInterface::class) === true) {
131
            /** @var LoggerInterface $logger */
132 3
            $logger = $container->get(LoggerInterface::class);
133
134
            // The sad truth is that when you have a problem logging might not be available (e.g. no permissions
135
            // to write on a disk). We can't do much with it and can only hope that the error information will be
136
            // delivered to the user other way.
137
            try {
138 3
                $logger->critical($message, ['exception' => $exception]);
139 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...
140
            }
141
        }
142
    }
143
144
    /**
145
     * @param Throwable $throwable
146
     * @param string    $text
147
     * @param int       $status
148
     *
149
     * @return ThrowableResponseInterface
150
     */
151 1
    private function createThrowableTextResponse(
152
        Throwable $throwable,
153
        string $text,
154
        int $status
155
    ): ThrowableResponseInterface {
156
        return new class ($throwable, $text, $status) extends TextResponse implements ThrowableResponseInterface
157
        {
158
            use ThrowableResponseTrait;
159
160
            /**
161
             * @param Throwable $throwable
162
             * @param string    $text
163
             * @param int       $status
164
             */
165
            public function __construct(Throwable $throwable, string $text, int $status)
166
            {
167 1
                parent::__construct($text, $status);
168 1
                $this->setThrowable($throwable);
169
            }
170
        };
171
    }
172
173
    /**
174
     * @param Throwable $throwable
175
     * @param string    $text
176
     * @param int       $status
177
     *
178
     * @return ThrowableResponseInterface
179
     */
180
    private function createThrowableHtmlResponse(
181
        Throwable $throwable,
182
        string $text,
183
        int $status
184
    ): ThrowableResponseInterface {
185
        return new class ($throwable, $text, $status) extends HtmlResponse implements ThrowableResponseInterface
186
        {
187
            use ThrowableResponseTrait;
188
189
            /**
190
             * @param Throwable $throwable
191
             * @param string    $text
192
             * @param int       $status
193
             */
194 2
            public function __construct(Throwable $throwable, string $text, int $status)
195
            {
196 2
                parent::__construct($text, $status);
197 2
                $this->setThrowable($throwable);
198
            }
199
        };
200
    }
201
}
202