Completed
Pull Request — v1 (#450)
by Denis
05:48
created

PrettyPageHandler::getEditor()   C

Complexity

Conditions 13
Paths 11

Size

Total Lines 32
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 20.2458

Importance

Changes 0
Metric Value
dl 0
loc 32
ccs 13
cts 20
cp 0.65
rs 5.1234
c 0
b 0
f 0
cc 13
eloc 16
nc 11
nop 2
crap 20.2458

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Whoops - php errors for cool kids
4
 * @author Filipe Dobreira <http://github.com/filp>
5
 */
6
7
namespace Whoops\Handler;
8
9
use InvalidArgumentException;
10
use RuntimeException;
11
use Symfony\Component\VarDumper\Cloner\AbstractCloner;
12
use Symfony\Component\VarDumper\Cloner\VarCloner;
13
use UnexpectedValueException;
14
use Whoops\Exception\Formatter;
15
use Whoops\Util\Misc;
16
use Whoops\Util\TemplateHelper;
17
18
class PrettyPageHandler extends Handler
19
{
20
    /**
21
     * Search paths to be scanned for resources, in the reverse
22
     * order they're declared.
23
     *
24
     * @var array
25
     */
26
    private $searchPaths = [];
27
28
    /**
29
     * Fast lookup cache for known resource locations.
30
     *
31
     * @var array
32
     */
33
    private $resourceCache = [];
34
35
    /**
36
     * The name of the custom css file.
37
     *
38
     * @var string
39
     */
40
    private $customCss = null;
41
42
    /**
43
     * @var array[]
44
     */
45
    private $extraTables = [];
46
47
    /**
48
     * @var bool
49
     */
50
    private $handleUnconditionally = false;
51
52
    /**
53
     * @var string
54
     */
55
    private $pageTitle = "Whoops! There was an error.";
56
57
    /**
58
     * A string identifier for a known IDE/text editor, or a closure
59
     * that resolves a string that can be used to open a given file
60
     * in an editor. If the string contains the special substrings
61
     * %file or %line, they will be replaced with the correct data.
62
     *
63
     * @example
64
     *  "txmt://open?url=%file&line=%line"
65
     * @var mixed $editor
66
     */
67
    protected $editor;
68
69
    /**
70
     * A list of known editor strings
71
     * @var array
72
     */
73
    protected $editors = [
74
        "sublime"  => "subl://open?url=file://%file&line=%line",
75
        "textmate" => "txmt://open?url=file://%file&line=%line",
76
        "emacs"    => "emacs://open?url=file://%file&line=%line",
77
        "macvim"   => "mvim://open/?url=file://%file&line=%line",
78
        "phpstorm" => "phpstorm://open?file=%file&line=%line",
79
    ];
80
81
    /**
82
     * Constructor.
83
     */
84 1
    public function __construct()
85
    {
86 1
        if (ini_get('xdebug.file_link_format') || extension_loaded('xdebug')) {
87
            // Register editor using xdebug's file_link_format option.
88
            $this->editors['xdebug'] = function ($file, $line) {
89 1
                return str_replace(['%f', '%l'], [$file, $line], ini_get('xdebug.file_link_format'));
90
            };
91 1
        }
92
93
        // Add the default, local resource search path:
94 1
        $this->searchPaths[] = __DIR__ . "/../Resources";
95 1
    }
96
97
    /**
98
     * @return int|null
99
     */
100 1
    public function handle()
101
    {
102 1
        if (!$this->handleUnconditionally()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->handleUnconditionally() of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
103
            // Check conditions for outputting HTML:
104
            // @todo: Make this more robust
105 1
            if (php_sapi_name() === 'cli') {
106
                // Help users who have been relying on an internal test value
107
                // fix their code to the proper method
108 1
                if (isset($_ENV['whoops-test'])) {
109
                    throw new \Exception(
110
                        'Use handleUnconditionally instead of whoops-test'
111
                        .' environment variable'
112
                    );
113
                }
114
115 1
                return Handler::DONE;
116
            }
117
        }
118
119
        // @todo: Make this more dynamic
120
        $helper = new TemplateHelper();
121
122
        if (class_exists('Symfony\Component\VarDumper\Cloner\VarCloner')) {
123
            $cloner = new VarCloner();
124
            // Only dump object internals if a custom caster exists.
125
            $cloner->addCasters(['*' => function ($obj, $a, $stub, $isNested, $filter = 0) {
0 ignored issues
show
Unused Code introduced by
The parameter $isNested 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...
Unused Code introduced by
The parameter $filter 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...
126
                $class = $stub->class;
127
                $classes = [$class => $class] + class_parents($class) + class_implements($class);
128
129
                foreach ($classes as $class) {
130
                    if (isset(AbstractCloner::$defaultCasters[$class])) {
131
                        return $a;
132
                    }
133
                }
134
135
                // Remove all internals
136
                return [];
137
            }]);
138
            $helper->setCloner($cloner);
139
        }
140
141
        $templateFile = $this->getResource("views/layout.html.php");
142
        $cssFile      = $this->getResource("css/whoops.base.css");
143
        $zeptoFile    = $this->getResource("js/zepto.min.js");
144
        $clipboard    = $this->getResource("js/clipboard.min.js");
145
        $jsFile       = $this->getResource("js/whoops.base.js");
146
147
        if ($this->customCss) {
148
            $customCssFile = $this->getResource($this->customCss);
149
        }
150
151
        $inspector = $this->getInspector();
152
        $frames    = $inspector->getFrames();
153
154
        $code = $inspector->getException()->getCode();
155
156
        if ($inspector->getException() instanceof \ErrorException) {
157
            // ErrorExceptions wrap the php-error types within the "severity" property
158
            $code = Misc::translateErrorCode($inspector->getException()->getSeverity());
159
        }
160
161
        // List of variables that will be passed to the layout template.
162
        $vars = [
163
            "page_title" => $this->getPageTitle(),
164
165
            // @todo: Asset compiler
166
            "stylesheet" => file_get_contents($cssFile),
167
            "zepto"      => file_get_contents($zeptoFile),
168
            "clipboard"  => file_get_contents($clipboard),
169
            "javascript" => file_get_contents($jsFile),
170
171
            // Template paths:
172
            "header"      => $this->getResource("views/header.html.php"),
173
            "frame_list"  => $this->getResource("views/frame_list.html.php"),
174
            "frame_code"  => $this->getResource("views/frame_code.html.php"),
175
            "env_details" => $this->getResource("views/env_details.html.php"),
176
177
            "title"          => $this->getPageTitle(),
178
            "name"           => explode("\\", $inspector->getExceptionName()),
179
            "message"        => $inspector->getException()->getMessage(),
180
            "code"           => $code,
181
            "plain_exception" => Formatter::formatExceptionPlain($inspector),
182
            "frames"         => $frames,
183
            "has_frames"     => !!count($frames),
184
            "handler"        => $this,
185
            "handlers"       => $this->getRun()->getHandlers(),
186
187
            "tables"      => [
188
                "GET Data"              => $_GET,
189
                "POST Data"             => $_POST,
190
                "Files"                 => $_FILES,
191
                "Cookies"               => $_COOKIE,
192
                "Session"               => isset($_SESSION) ? $_SESSION :  [],
193
                "Server/Request Data"   => $_SERVER,
194
                "Environment Variables" => $_ENV,
195
            ],
196
        ];
197
198
        if (isset($customCssFile)) {
199
            $vars["stylesheet"] .= file_get_contents($customCssFile);
200
        }
201
202
        // Add extra entries list of data tables:
203
        // @todo: Consolidate addDataTable and addDataTableCallback
204
        $extraTables = array_map(function ($table) use ($inspector) {
205
            return $table instanceof \Closure ? $table($inspector) : $table;
206
        }, $this->getDataTables());
207
        $vars["tables"] = array_merge($extraTables, $vars["tables"]);
208
209
        if (\Whoops\Util\Misc::canSendHeaders()) {
210
            header('Content-Type: text/html');
211
        }
212
213
        $plainTextHandler = new PlainTextHandler();
214
        $plainTextHandler->setException($this->getException());
215
        $plainTextHandler->setInspector($this->getInspector());
216
        $vars["preface"] = "<!--\n\n\n" . $plainTextHandler->generateResponse() . "\n\n\n\n\n\n\n\n\n\n\n-->";
217
218
        $helper->setVariables($vars);
219
        $helper->render($templateFile);
220
221
        return Handler::QUIT;
222
    }
223
224
    /**
225
     * Adds an entry to the list of tables displayed in the template.
226
     * The expected data is a simple associative array. Any nested arrays
227
     * will be flattened with print_r
228
     * @param string $label
229
     * @param array  $data
230
     */
231 1
    public function addDataTable($label, array $data)
232
    {
233 1
        $this->extraTables[$label] = $data;
234 1
    }
235
236
    /**
237
     * Lazily adds an entry to the list of tables displayed in the table.
238
     * The supplied callback argument will be called when the error is rendered,
239
     * it should produce a simple associative array. Any nested arrays will
240
     * be flattened with print_r.
241
     *
242
     * @throws InvalidArgumentException If $callback is not callable
243
     * @param  string                   $label
244
     * @param  callable                 $callback Callable returning an associative array
245
     */
246 1
    public function addDataTableCallback($label, /* callable */ $callback)
247
    {
248 1
        if (!is_callable($callback)) {
249
            throw new InvalidArgumentException('Expecting callback argument to be callable');
250
        }
251
252 1
        $this->extraTables[$label] = function (\Whoops\Exception\Inspector $inspector = null) use ($callback) {
253
            try {
254 1
                $result = call_user_func($callback, $inspector);
255
256
                // Only return the result if it can be iterated over by foreach().
257 1
                return is_array($result) || $result instanceof \Traversable ? $result : [];
258
            } catch (\Exception $e) {
259
                // Don't allow failure to break the rendering of the original exception.
260
                return [];
261
            }
262
        };
263 1
    }
264
265
    /**
266
     * Returns all the extra data tables registered with this handler.
267
     * Optionally accepts a 'label' parameter, to only return the data
268
     * table under that label.
269
     * @param  string|null      $label
270
     * @return array[]|callable
271
     */
272 2
    public function getDataTables($label = null)
273
    {
274 2
        if ($label !== null) {
275 2
            return isset($this->extraTables[$label]) ?
276 2
                   $this->extraTables[$label] : [];
277
        }
278
279 2
        return $this->extraTables;
280
    }
281
282
    /**
283
     * Allows to disable all attempts to dynamically decide whether to
284
     * handle or return prematurely.
285
     * Set this to ensure that the handler will perform no matter what.
286
     * @param  bool|null $value
287
     * @return bool|null
288
     */
289 1
    public function handleUnconditionally($value = null)
290
    {
291 1
        if (func_num_args() == 0) {
292 1
            return $this->handleUnconditionally;
293
        }
294
295
        $this->handleUnconditionally = (bool) $value;
296
    }
297
298
    /**
299
     * Adds an editor resolver, identified by a string
300
     * name, and that may be a string path, or a callable
301
     * resolver. If the callable returns a string, it will
302
     * be set as the file reference's href attribute.
303
     *
304
     * @example
305
     *  $run->addEditor('macvim', "mvim://open?url=file://%file&line=%line")
306
     * @example
307
     *   $run->addEditor('remove-it', function($file, $line) {
308
     *       unlink($file);
309
     *       return "http://stackoverflow.com";
310
     *   });
311
     * @param string $identifier
312
     * @param string $resolver
313
     */
314 1
    public function addEditor($identifier, $resolver)
315
    {
316 1
        $this->editors[$identifier] = $resolver;
317 1
    }
318
319
    /**
320
     * Set the editor to use to open referenced files, by a string
321
     * identifier, or a callable that will be executed for every
322
     * file reference, with a $file and $line argument, and should
323
     * return a string.
324
     *
325
     * @example
326
     *   $run->setEditor(function($file, $line) { return "file:///{$file}"; });
327
     * @example
328
     *   $run->setEditor('sublime');
329
     *
330
     * @throws InvalidArgumentException If invalid argument identifier provided
331
     * @param  string|callable          $editor
332
     */
333 4
    public function setEditor($editor)
334
    {
335 4
        if (!is_callable($editor) && !isset($this->editors[$editor])) {
336
            throw new InvalidArgumentException(
337
                "Unknown editor identifier: $editor. Known editors:" .
338
                implode(",", array_keys($this->editors))
339
            );
340
        }
341
342 4
        $this->editor = $editor;
343 4
    }
344
345
    /**
346
     * Given a string file path, and an integer file line,
347
     * executes the editor resolver and returns, if available,
348
     * a string that may be used as the href property for that
349
     * file reference.
350
     *
351
     * @throws InvalidArgumentException If editor resolver does not return a string
352
     * @param  string                   $filePath
353
     * @param  int                      $line
354
     * @return string|bool
355
     */
356 4
    public function getEditorHref($filePath, $line)
357
    {
358 4
        $editor = $this->getEditor($filePath, $line);
359
360 4
        if (!$editor) {
361
            return false;
362
        }
363
364
        // Check that the editor is a string, and replace the
365
        // %line and %file placeholders:
366 4
        if (!isset($editor['url']) || !is_string($editor['url'])) {
367
            throw new UnexpectedValueException(
368
                __METHOD__ . " should always resolve to a string or a valid editor array; got something else instead."
369
            );
370
        }
371
372 4
        $editor['url'] = str_replace("%line", rawurlencode($line), $editor['url']);
373 4
        $editor['url'] = str_replace("%file", rawurlencode($filePath), $editor['url']);
374
375 4
        return $editor['url'];
376
    }
377
378
    /**
379
     * Given a boolean if the editor link should
380
     * act as an Ajax request. The editor must be a
381
     * valid callable function/closure
382
     *
383
     * @throws UnexpectedValueException  If editor resolver does not return a boolean
384
     * @param  string                   $filePath
385
     * @param  int                      $line
386
     * @return bool
387
     */
388 1
    public function getEditorAjax($filePath, $line)
389
    {
390 1
        $editor = $this->getEditor($filePath, $line);
391
392
        // Check that the ajax is a bool
393 1
        if (!isset($editor['ajax']) || !is_bool($editor['ajax'])) {
394
            throw new UnexpectedValueException(
395
                __METHOD__ . " should always resolve to a bool; got something else instead."
396
            );
397
        }
398 1
        return $editor['ajax'];
399
    }
400
401
    /**
402
     * Given a boolean if the editor link should
403
     * act as an Ajax request. The editor must be a
404
     * valid callable function/closure
405
     *
406
     * @throws UnexpectedValueException  If editor resolver does not return a boolean
407
     * @param  string                   $filePath
408
     * @param  int                      $line
409
     * @return mixed
410
     */
411 1
    protected function getEditor($filePath, $line)
412
    {
413 1
        if ($this->editor === null && !is_string($this->editor) && !is_callable($this->editor))
414 1
        {
415
            return false;
416
        }
417 1
        else if(is_string($this->editor) && isset($this->editors[$this->editor]) && !is_callable($this->editors[$this->editor]))
418 1
        {
419
           return [
420
                'ajax' => false,
421
                'url' => $this->editors[$this->editor],
422
            ];
423
        }
424 1
        else if(is_callable($this->editor) || (isset($this->editors[$this->editor]) && is_callable($this->editors[$this->editor])))
425 1
        {
426 1
            if(is_callable($this->editor))
427 1
            {
428
                $callback = call_user_func($this->editor, $filePath, $line);
429
            }
430
            else
431
            {
432 1
                $callback = call_user_func($this->editors[$this->editor], $filePath, $line);
433
            }
434
435
            return [
436 1
                'ajax' => isset($callback['ajax']) ? $callback['ajax'] : false,
437 1
                'url' => (is_array($callback) ? $callback['url'] : $callback),
438 1
            ];
439
        }
440
441
        return false;
442
    }
443
444
    /**
445
     * @param  string $title
446
     * @return void
447
     */
448 1
    public function setPageTitle($title)
449
    {
450 1
        $this->pageTitle = (string) $title;
451 1
    }
452
453
    /**
454
     * @return string
455
     */
456 1
    public function getPageTitle()
457
    {
458 1
        return $this->pageTitle;
459
    }
460
461
    /**
462
     * Adds a path to the list of paths to be searched for
463
     * resources.
464
     *
465
     * @throws InvalidArgumnetException If $path is not a valid directory
466
     *
467
     * @param  string $path
468
     * @return void
469
     */
470 2
    public function addResourcePath($path)
471
    {
472 2
        if (!is_dir($path)) {
473 1
            throw new InvalidArgumentException(
474 1
                "'$path' is not a valid directory"
475 1
            );
476
        }
477
478 1
        array_unshift($this->searchPaths, $path);
479 1
    }
480
481
    /**
482
     * Adds a custom css file to be loaded.
483
     *
484
     * @param  string $name
485
     * @return void
486
     */
487
    public function addCustomCss($name)
488
    {
489
        $this->customCss = $name;
490
    }
491
492
    /**
493
     * @return array
494
     */
495 1
    public function getResourcePaths()
496
    {
497 1
        return $this->searchPaths;
498
    }
499
500
    /**
501
     * Finds a resource, by its relative path, in all available search paths.
502
     * The search is performed starting at the last search path, and all the
503
     * way back to the first, enabling a cascading-type system of overrides
504
     * for all resources.
505
     *
506
     * @throws RuntimeException If resource cannot be found in any of the available paths
507
     *
508
     * @param  string $resource
509
     * @return string
510
     */
511
    protected function getResource($resource)
512
    {
513
        // If the resource was found before, we can speed things up
514
        // by caching its absolute, resolved path:
515
        if (isset($this->resourceCache[$resource])) {
516
            return $this->resourceCache[$resource];
517
        }
518
519
        // Search through available search paths, until we find the
520
        // resource we're after:
521
        foreach ($this->searchPaths as $path) {
522
            $fullPath = $path . "/$resource";
523
524
            if (is_file($fullPath)) {
525
                // Cache the result:
526
                $this->resourceCache[$resource] = $fullPath;
527
                return $fullPath;
528
            }
529
        }
530
531
        // If we got this far, nothing was found.
532
        throw new RuntimeException(
533
            "Could not find resource '$resource' in any resource paths."
534
            . "(searched: " . join(", ", $this->searchPaths). ")"
535
        );
536
    }
537
538
    /**
539
     * @deprecated
540
     *
541
     * @return string
542
     */
543
    public function getResourcesPath()
544
    {
545
        $allPaths = $this->getResourcePaths();
546
547
        // Compat: return only the first path added
548
        return end($allPaths) ?: null;
549
    }
550
551
    /**
552
     * @deprecated
553
     *
554
     * @param  string $resourcesPath
555
     * @return void
556
     */
557
    public function setResourcesPath($resourcesPath)
558
    {
559
        $this->addResourcePath($resourcesPath);
560
    }
561
}
562