Completed
Pull Request — master (#640)
by Michał
01:37
created

Run   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 458
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 72.26%

Importance

Changes 0
Metric Value
wmc 52
lcom 1
cbo 6
dl 0
loc 458
ccs 112
cts 155
cp 0.7226
rs 7.44
c 0
b 0
f 0

21 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 2
A pushHandler() 0 4 1
A prependHandler() 0 5 1
A resolveHandler() 0 15 3
A appendHandler() 0 5 1
A popHandler() 0 4 1
A shiftHandler() 0 4 1
A getHandlers() 0 4 1
A clearHandlers() 0 5 1
A getInspector() 0 4 1
A register() 0 19 2
A unregister() 0 11 2
A allowQuit() 0 8 2
A silenceErrorsInPaths() 0 16 1
A getSilenceErrorsInPaths() 0 4 1
B sendHttpCode() 0 22 6
A writeToOutput() 0 8 2
C handleException() 0 73 11
B handleError() 0 29 6
A handleShutdown() 0 20 3
A writeToOutputNow() 0 12 3

How to fix   Complexity   

Complex Class

Complex classes like Run often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Run, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Whoops - php errors for cool kids
4
 * @author Filipe Dobreira <http://github.com/filp>
5
 */
6
7
namespace Whoops;
8
9
use InvalidArgumentException;
10
use Whoops\Exception\ErrorException;
11
use Whoops\Exception\Inspector;
12
use Whoops\Handler\CallbackHandler;
13
use Whoops\Handler\Handler;
14
use Whoops\Handler\HandlerInterface;
15
use Whoops\Util\Misc;
16
use Whoops\Util\SystemFacade;
17
18
final class Run implements RunInterface
19
{
20
    private $isRegistered;
21
    private $allowQuit       = true;
22
    private $sendOutput      = true;
23
24
    /**
25
     * @var integer|false
26
     */
27
    private $sendHttpCode    = 500;
28
29
    /**
30
     * @var HandlerInterface[]
31
     */
32
    private $handlerStack = [];
33
34
    private $silencedPatterns = [];
35
36
    private $system;
37
38 8
    public function __construct(SystemFacade $system = null)
39
    {
40 8
        $this->system = $system ?: new SystemFacade;
41 8
    }
42
43
    /**
44
     * Pushes a handler to the end of the stack
45
     *
46
     * @throws InvalidArgumentException  If argument is not callable or instance of HandlerInterface
47
     * @param  Callable|HandlerInterface $handler
48
     * @return Run
49
     * @deprecated use appendHandler and prependHandler instead
50
     */
51 10
    public function pushHandler($handler)
52
    {
53 10
        return $this->prependHandler($handler);
54
    }
55
56
    /**
57
     * Adds a handler to be executed as last.
58
     *
59
     * @throws InvalidArgumentException  If argument is not callable or instance of HandlerInterface
60
     * @param  Callable|HandlerInterface $handler
61
     * @return Run
62
     */
63 1
    public function appendHandler($handler)
64
    {
65 1
        array_unshift($this->handlerStack, $this->resolveHandler($handler));
66 1
        return $this;
67
    }
68
69
    /**
70
     * Adds a handler to be executed as first.
71
     *
72
     * @throws InvalidArgumentException  If argument is not callable or instance of HandlerInterface
73
     * @param  Callable|HandlerInterface $handler
74
     * @return Run
75
     */
76 7
    public function prependHandler($handler)
77
    {
78 7
        $this->handlerStack[] = $this->resolveHandler($handler);
79 7
        return $this;
80
    }
81
82
    /**
83
     * Create a CallbackHandler from callable and throw if handler is invalid
84
     *
85
     * @throws InvalidArgumentException  If argument is not callable or instance of HandlerInterface
86
     * @param Callable|HandlerInterface $handler
87
     * @return HandlerInterface
88
     */
89 8
    private function resolveHandler($handler)
90
    {
91 8
        if (is_callable($handler)) {
92
            $handler = new CallbackHandler($handler);
93
        }
94
95 8
        if (!$handler instanceof HandlerInterface) {
96
            throw new InvalidArgumentException(
97
                "Argument to " . __METHOD__ . " must be a callable, or instance of "
98
                . "Whoops\\Handler\\HandlerInterface"
99
            );
100
        }
101
102 8
        return $handler;
103
    }
104
105
    /**
106
     * Removes the last handler in the stack and returns it.
107
     * As implementation is using stack, the last element added to the stack
108
     * is executed as first.
109
     *
110
     * Returns null if there"s nothing else to pop.
111
     * @return null|HandlerInterface
112
     */
113 1
    public function popHandler()
114
    {
115 1
        return array_pop($this->handlerStack);
116
    }
117
118
    /**
119
     * Removes the first handler in the stack and returns it.
120
     * As implementation is using stack, the first element added to the stack
121
     * is executed as last.
122
     *
123
     * Returns null if there"s nothing else to shift.
124
     * @return null|HandlerInterface
125
     */
126
    public function shiftHandler()
127
    {
128
        return array_shift($this->handlerStack);
129
    }
130
131
    /**
132
     * Returns an array with all handlers, in the
133
     * order they were added to the stack.
134
     *
135
     * As stack implementation is used, handlers are executed from the end of that list
136
     * (from the top of the stack).
137
     *
138
     * You can use:
139
     * - appendHandler to add handler to be executed as last (add to the top of the stack)
140
     * - prependHandler to add the handler to be executed as first (add to the bottom of the stack)
141
     *
142
     * @return array
143
     */
144 2
    public function getHandlers()
145
    {
146 2
        return $this->handlerStack;
147
    }
148
149
    /**
150
     * Clears all handlers in the handlerStack, including
151
     * the default PrettyPage handler.
152
     * @return Run
153
     */
154 1
    public function clearHandlers()
155
    {
156 1
        $this->handlerStack = [];
157 1
        return $this;
158
    }
159
160
    /**
161
     * @param  \Throwable $exception
162
     * @return Inspector
163
     */
164 5
    private function getInspector($exception)
165
    {
166 5
        return new Inspector($exception);
167
    }
168
169
    /**
170
     * Registers this instance as an error handler.
171
     * @return Run
172
     */
173 6
    public function register()
174
    {
175 6
        if (!$this->isRegistered) {
176
            // Workaround PHP bug 42098
177
            // https://bugs.php.net/bug.php?id=42098
178 6
            class_exists("\\Whoops\\Exception\\ErrorException");
179 6
            class_exists("\\Whoops\\Exception\\FrameCollection");
180 6
            class_exists("\\Whoops\\Exception\\Frame");
181 6
            class_exists("\\Whoops\\Exception\\Inspector");
182
183 6
            $this->system->setErrorHandler([$this, self::ERROR_HANDLER]);
184 6
            $this->system->setExceptionHandler([$this, self::EXCEPTION_HANDLER]);
185 6
            $this->system->registerShutdownFunction([$this, self::SHUTDOWN_HANDLER]);
186
187 6
            $this->isRegistered = true;
188 6
        }
189
190 6
        return $this;
191
    }
192
193
    /**
194
     * Unregisters all handlers registered by this Whoops\Run instance
195
     * @return Run
196
     */
197 1
    public function unregister()
198
    {
199 1
        if ($this->isRegistered) {
200 1
            $this->system->restoreExceptionHandler();
201 1
            $this->system->restoreErrorHandler();
202
203 1
            $this->isRegistered = false;
204 1
        }
205
206 1
        return $this;
207
    }
208
209
    /**
210
     * Should Whoops allow Handlers to force the script to quit?
211
     * @param  bool|int $exit
212
     * @return bool
213
     */
214 8
    public function allowQuit($exit = null)
215
    {
216 8
        if (func_num_args() == 0) {
217 3
            return $this->allowQuit;
218
        }
219
220 8
        return $this->allowQuit = (bool) $exit;
221
    }
222
223
    /**
224
     * Silence particular errors in particular files
225
     * @param  array|string $patterns List or a single regex pattern to match
226
     * @param  int          $levels   Defaults to E_STRICT | E_DEPRECATED
227
     * @return \Whoops\Run
228
     */
229 1
    public function silenceErrorsInPaths($patterns, $levels = 10240)
230
    {
231 1
        $this->silencedPatterns = array_merge(
232 1
            $this->silencedPatterns,
233 1
            array_map(
234 1
                function ($pattern) use ($levels) {
235
                    return [
236 1
                        "pattern" => $pattern,
237 1
                        "levels" => $levels,
238 1
                    ];
239 1
                },
240
                (array) $patterns
241 1
            )
242 1
        );
243 1
        return $this;
244
    }
245
246
247
    /**
248
     * Returns an array with silent errors in path configuration
249
     *
250
     * @return array
251
     */
252
    public function getSilenceErrorsInPaths()
253
    {
254
        return $this->silencedPatterns;
255
    }
256
257
    /*
258
     * Should Whoops send HTTP error code to the browser if possible?
259
     * Whoops will by default send HTTP code 500, but you may wish to
260
     * use 502, 503, or another 5xx family code.
261
     *
262
     * @param bool|int $code
263
     * @return int|false
264
     */
265 8
    public function sendHttpCode($code = null)
266
    {
267 8
        if (func_num_args() == 0) {
268 6
            return $this->sendHttpCode;
269
        }
270
271 3
        if (!$code) {
272 1
            return $this->sendHttpCode = false;
273
        }
274
275 2
        if ($code === true) {
276 1
            $code = 500;
277 1
        }
278
279 2
        if ($code < 400 || 600 <= $code) {
280 1
            throw new InvalidArgumentException(
281 1
                 "Invalid status code '$code', must be 4xx or 5xx"
282 1
            );
283
        }
284
285 1
        return $this->sendHttpCode = $code;
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->sendHttpCode = $code; of type boolean|integer adds the type boolean to the return on line 285 which is incompatible with the return type declared by the interface Whoops\RunInterface::sendHttpCode of type integer|false.
Loading history...
286
    }
287
288
    /**
289
     * Should Whoops push output directly to the client?
290
     * If this is false, output will be returned by handleException
291
     * @param  bool|int $send
292
     * @return bool
293
     */
294 7
    public function writeToOutput($send = null)
295
    {
296 7
        if (func_num_args() == 0) {
297 7
            return $this->sendOutput;
298
        }
299
300 1
        return $this->sendOutput = (bool) $send;
301
    }
302
303
    /**
304
     * Handles an exception, ultimately generating a Whoops error
305
     * page.
306
     *
307
     * @param  \Throwable $exception
308
     * @return string     Output generated by handlers
309
     */
310 9
    public function handleException($exception)
311
    {
312
        // Walk the registered handlers in the reverse order
313
        // they were registered, and pass off the exception
314 9
        $inspector = $this->getInspector($exception);
315
316
        // Capture output produced while handling the exception,
317
        // we might want to send it straight away to the client,
318
        // or return it silently.
319 9
        $this->system->startOutputBuffering();
320
321
        // Just in case there are no handlers:
322 9
        $handlerResponse = null;
323 9
        $handlerContentType = null;
324
325
        try {
326 9
            foreach (array_reverse($this->handlerStack) as $handler) {
327 9
                $handler->setRun($this);
328 9
                $handler->setInspector($inspector);
329 9
                $handler->setException($exception);
330
331
                // The HandlerInterface does not require an Exception passed to handle()
332
                // and neither of our bundled handlers use it.
333
                // However, 3rd party handlers may have already relied on this parameter,
334
                // and removing it would be possibly breaking for users.
335 9
                $handlerResponse = $handler->handle($exception);
336
337
                // Collect the content type for possible sending in the headers.
338 9
                $handlerContentType = method_exists($handler, 'contentType') ? $handler->contentType() : null;
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Whoops\Handler\HandlerInterface as the method contentType() does only exist in the following implementations of said interface: Whoops\Handler\JsonResponseHandler, Whoops\Handler\PlainTextHandler, Whoops\Handler\PrettyPageHandler, Whoops\Handler\XmlResponseHandler.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
339
340 9
                if (in_array($handlerResponse, [Handler::LAST_HANDLER, Handler::QUIT])) {
341
                    // The Handler has handled the exception in some way, and
342
                    // wishes to quit execution (Handler::QUIT), or skip any
343
                    // other handlers (Handler::LAST_HANDLER). If $this->allowQuit
344
                    // is false, Handler::QUIT behaves like Handler::LAST_HANDLER
345 4
                    break;
346
                }
347 9
            }
348
349 9
            $willQuit = $handlerResponse == Handler::QUIT && $this->allowQuit();
350 9
        } finally {
351 9
            $output = $this->system->cleanOutputBuffer();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression $this->system->cleanOutputBuffer(); of type string|false adds false to the return on line 381 which is incompatible with the return type declared by the interface Whoops\RunInterface::handleException of type string. It seems like you forgot to handle an error condition.
Loading history...
352 9
        }
353
354
        // If we're allowed to, send output generated by handlers directly
355
        // to the output, otherwise, and if the script doesn't quit, return
356
        // it so that it may be used by the caller
357 9
        if ($this->writeToOutput()) {
358
            // @todo Might be able to clean this up a bit better
359 8
            if ($willQuit) {
0 ignored issues
show
Bug introduced by
The variable $willQuit does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
360
                // Cleanup all other output buffers before sending our output:
361
                while ($this->system->getOutputBufferLevel() > 0) {
362
                    $this->system->endOutputBuffering();
363
                }
364
365
                // Send any headers if needed:
366
                if (Misc::canSendHeaders() && $handlerContentType) {
367
                    header("Content-Type: {$handlerContentType}");
368
                }
369
            }
370
371 8
            $this->writeToOutputNow($output);
0 ignored issues
show
Security Bug introduced by
It seems like $output defined by $this->system->cleanOutputBuffer() on line 351 can also be of type false; however, Whoops\Run::writeToOutputNow() 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...
372 8
        }
373
374 9
        if ($willQuit) {
375
            // HHVM fix for https://github.com/facebook/hhvm/issues/4055
376
            $this->system->flushOutputBuffer();
377
378
            $this->system->stopExecution(1);
379
        }
380
381 9
        return $output;
382
    }
383
384
    /**
385
     * Converts generic PHP errors to \ErrorException
386
     * instances, before passing them off to be handled.
387
     *
388
     * This method MUST be compatible with set_error_handler.
389
     *
390
     * @param int    $level
391
     * @param string $message
392
     * @param string $file
393
     * @param int    $line
394
     *
395
     * @return bool
396
     * @throws ErrorException
397
     */
398 5
    public function handleError($level, $message, $file = null, $line = null)
399
    {
400 5
        if ($level & $this->system->getErrorReportingLevel()) {
401 2
            foreach ($this->silencedPatterns as $entry) {
402
                $pathMatches = (bool) preg_match($entry["pattern"], $file);
403
                $levelMatches = $level & $entry["levels"];
404
                if ($pathMatches && $levelMatches) {
405
                    // Ignore the error, abort handling
406
                    // See https://github.com/filp/whoops/issues/418
407
                    return true;
408
                }
409 2
            }
410
411
            // XXX we pass $level for the "code" param only for BC reasons.
412
            // see https://github.com/filp/whoops/issues/267
413 2
            $exception = new ErrorException($message, /*code*/ $level, /*severity*/ $level, $file, $line);
414 2
            if ($this->canThrowExceptions) {
415 2
                throw $exception;
416
            } else {
417
                $this->handleException($exception);
0 ignored issues
show
Documentation introduced by
$exception is of type object<Whoops\Exception\ErrorException>, but the function expects a object<Throwable>.

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...
418
            }
419
            // Do not propagate errors which were already handled by Whoops.
420
            return true;
421
        }
422
423
        // Propagate error to the next handler, allows error_get_last() to
424
        // work on silenced errors.
425 3
        return false;
426
    }
427
428
    /**
429
     * Special case to deal with Fatal errors and the like.
430
     */
431
    public function handleShutdown()
432
    {
433
        // If we reached this step, we are in shutdown handler.
434
        // An exception thrown in a shutdown handler will not be propagated
435
        // to the exception handler. Pass that information along.
436
        $this->canThrowExceptions = false;
437
438
        $error = $this->system->getLastError();
439
        if ($error && Misc::isLevelFatal($error['type'])) {
440
            // If there was a fatal error,
441
            // it was not handled in handleError yet.
442
            $this->allowQuit = false;
443
            $this->handleError(
444
                $error['type'],
445
                $error['message'],
446
                $error['file'],
447
                $error['line']
448
            );
449
        }
450
    }
451
452
    /**
453
     * In certain scenarios, like in shutdown handler, we can not throw exceptions
454
     * @var bool
455
     */
456
    private $canThrowExceptions = true;
457
458
    /**
459
     * Echo something to the browser
460
     * @param  string $output
461
     * @return $this
462
     */
463 5
    private function writeToOutputNow($output)
464
    {
465 5
        if ($this->sendHttpCode() && \Whoops\Util\Misc::canSendHeaders()) {
466
            $this->system->setHttpResponseCode(
467
                $this->sendHttpCode()
0 ignored issues
show
Bug introduced by
It seems like $this->sendHttpCode() targeting Whoops\Run::sendHttpCode() can also be of type boolean; however, Whoops\Util\SystemFacade::setHttpResponseCode() does only seem to accept integer, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
468
            );
469
        }
470
471 5
        echo $output;
472
473 5
        return $this;
474
    }
475
}
476