yiisoft /
error-handler
| 1 | <?php |
||
| 2 | |||
| 3 | declare(strict_types=1); |
||
| 4 | |||
| 5 | namespace Yiisoft\ErrorHandler\Renderer; |
||
| 6 | |||
| 7 | use Alexkart\CurlBuilder\Command; |
||
| 8 | use cebe\markdown\GithubMarkdown; |
||
| 9 | use Psr\Http\Message\ServerRequestInterface; |
||
| 10 | use RuntimeException; |
||
| 11 | use Throwable; |
||
| 12 | use Yiisoft\ErrorHandler\CompositeException; |
||
| 13 | use Yiisoft\ErrorHandler\ErrorData; |
||
| 14 | use Yiisoft\ErrorHandler\Exception\ErrorException; |
||
| 15 | use Yiisoft\ErrorHandler\ThrowableRendererInterface; |
||
| 16 | use Yiisoft\FriendlyException\FriendlyExceptionInterface; |
||
| 17 | |||
| 18 | use function array_values; |
||
| 19 | use function dirname; |
||
| 20 | use function extract; |
||
| 21 | use function file; |
||
| 22 | use function file_exists; |
||
| 23 | use function func_get_arg; |
||
| 24 | use function glob; |
||
| 25 | use function htmlspecialchars; |
||
| 26 | use function implode; |
||
| 27 | use function is_array; |
||
| 28 | use function is_bool; |
||
| 29 | use function is_file; |
||
| 30 | use function is_object; |
||
| 31 | use function is_resource; |
||
| 32 | use function is_string; |
||
| 33 | use function ksort; |
||
| 34 | use function mb_strlen; |
||
| 35 | use function mb_substr; |
||
| 36 | use function ob_clean; |
||
| 37 | use function ob_end_clean; |
||
| 38 | use function ob_get_clean; |
||
| 39 | use function ob_get_level; |
||
| 40 | use function ob_implicit_flush; |
||
| 41 | use function ob_start; |
||
| 42 | use function realpath; |
||
| 43 | use function str_replace; |
||
| 44 | use function stripos; |
||
| 45 | use function strlen; |
||
| 46 | |||
| 47 | /** |
||
| 48 | * Formats throwable into HTML string. |
||
| 49 | * |
||
| 50 | * @psalm-import-type DebugBacktraceType from ErrorException |
||
| 51 | */ |
||
| 52 | final class HtmlRenderer implements ThrowableRendererInterface |
||
| 53 | { |
||
| 54 | private GithubMarkdown $markdownParser; |
||
| 55 | |||
| 56 | /** |
||
| 57 | * @var string The full path to the default template directory. |
||
| 58 | */ |
||
| 59 | private string $defaultTemplatePath; |
||
| 60 | |||
| 61 | /** |
||
| 62 | * @var string The full path of the template file for rendering exceptions without call stack information. |
||
| 63 | * |
||
| 64 | * This template should be used in production. |
||
| 65 | */ |
||
| 66 | private string $template; |
||
| 67 | |||
| 68 | /** |
||
| 69 | * @var string The full path of the template file for rendering exceptions with call stack information. |
||
| 70 | * |
||
| 71 | * This template should be used in development. |
||
| 72 | */ |
||
| 73 | private string $verboseTemplate; |
||
| 74 | |||
| 75 | /** |
||
| 76 | * @var int The maximum number of source code lines to be displayed. Defaults to 19. |
||
| 77 | */ |
||
| 78 | private int $maxSourceLines; |
||
| 79 | |||
| 80 | /** |
||
| 81 | * @var int The maximum number of trace source code lines to be displayed. Defaults to 13. |
||
| 82 | */ |
||
| 83 | private int $maxTraceLines; |
||
| 84 | |||
| 85 | /** |
||
| 86 | * @var string|null The trace header line with placeholders to be be substituted. Defaults to null. |
||
| 87 | * |
||
| 88 | * The placeholders are {file}, {line} and {icon}. A typical use case is the creation of IDE-specific links, |
||
| 89 | * since when you click on a trace header link, it opens directly in the IDE. You can also insert custom content. |
||
| 90 | * |
||
| 91 | * Example IDE link: |
||
| 92 | * |
||
| 93 | * ``` |
||
| 94 | * <a href="ide://open?file={file}&line={line}">{icon}</a> |
||
| 95 | * ``` |
||
| 96 | */ |
||
| 97 | private ?string $traceHeaderLine; |
||
| 98 | |||
| 99 | /** |
||
| 100 | * @var string[]|null The list of vendor paths is determined automatically. |
||
| 101 | * |
||
| 102 | * One path if the error handler is installed as a vendor package, or a list of package vendor paths |
||
| 103 | * if the error handler is installed for development in {@link https://github.com/yiisoft/yii-dev-tool}. |
||
| 104 | */ |
||
| 105 | private ?array $vendorPaths = null; |
||
| 106 | |||
| 107 | /** |
||
| 108 | * @param array $settings Settings can have the following keys: |
||
| 109 | * - template: string, full path of the template file for rendering exceptions without call stack information. |
||
| 110 | * - verboseTemplate: string, full path of the template file for rendering exceptions with call stack information. |
||
| 111 | * - maxSourceLines: int, maximum number of source code lines to be displayed. Defaults to 19. |
||
| 112 | * - maxTraceLines: int, maximum number of trace source code lines to be displayed. Defaults to 13. |
||
| 113 | * - traceHeaderLine: string, trace header line with placeholders to be be substituted. Defaults to null. |
||
| 114 | * |
||
| 115 | * @psalm-param array{ |
||
| 116 | * template?: string, |
||
| 117 | * verboseTemplate?: string, |
||
| 118 | * maxSourceLines?: int, |
||
| 119 | * maxTraceLines?: int, |
||
| 120 | * traceHeaderLine?: string, |
||
| 121 | * } $settings |
||
| 122 | */ |
||
| 123 | 39 | public function __construct(array $settings = []) |
|
| 124 | { |
||
| 125 | 39 | $this->markdownParser = new GithubMarkdown(); |
|
| 126 | 39 | $this->markdownParser->html5 = true; |
|
| 127 | |||
| 128 | 39 | $this->defaultTemplatePath = dirname(__DIR__, 2) . '/templates'; |
|
| 129 | 39 | $this->template = $settings['template'] ?? $this->defaultTemplatePath . '/production.php'; |
|
| 130 | 39 | $this->verboseTemplate = $settings['verboseTemplate'] ?? $this->defaultTemplatePath . '/development.php'; |
|
| 131 | 39 | $this->maxSourceLines = $settings['maxSourceLines'] ?? 19; |
|
| 132 | 39 | $this->maxTraceLines = $settings['maxTraceLines'] ?? 13; |
|
| 133 | 39 | $this->traceHeaderLine = $settings['traceHeaderLine'] ?? null; |
|
| 134 | } |
||
| 135 | |||
| 136 | 5 | public function render(Throwable $t, ServerRequestInterface $request = null): ErrorData |
|
| 137 | { |
||
| 138 | 5 | return new ErrorData($this->renderTemplate($this->template, [ |
|
| 139 | 5 | 'request' => $request, |
|
| 140 | 5 | 'throwable' => $t, |
|
| 141 | 5 | ])); |
|
| 142 | } |
||
| 143 | |||
| 144 | 4 | public function renderVerbose(Throwable $t, ServerRequestInterface $request = null): ErrorData |
|
| 145 | { |
||
| 146 | 4 | return new ErrorData($this->renderTemplate($this->verboseTemplate, [ |
|
| 147 | 4 | 'request' => $request, |
|
| 148 | 4 | 'throwable' => $t, |
|
| 149 | 4 | ])); |
|
| 150 | } |
||
| 151 | |||
| 152 | /** |
||
| 153 | * Encodes special characters into HTML entities for use as a content. |
||
| 154 | * |
||
| 155 | * @param string $content The content to be encoded. |
||
| 156 | * |
||
| 157 | * @return string Encoded content. |
||
| 158 | */ |
||
| 159 | 18 | public function htmlEncode(string $content): string |
|
| 160 | { |
||
| 161 | 18 | return htmlspecialchars($content, ENT_QUOTES, 'UTF-8'); |
|
| 162 | } |
||
| 163 | |||
| 164 | public function parseMarkdown(string $content): string |
||
| 165 | { |
||
| 166 | $html = $this->markdownParser->parse($content); |
||
| 167 | /** |
||
| 168 | * @psalm-suppress InvalidArgument |
||
| 169 | * |
||
| 170 | * @link https://github.com/vimeo/psalm/issues/4317 |
||
| 171 | */ |
||
| 172 | return strip_tags($html, [ |
||
| 173 | 'h1', |
||
| 174 | 'h2', |
||
| 175 | 'h3', |
||
| 176 | 'h4', |
||
| 177 | 'h5', |
||
| 178 | 'h6', |
||
| 179 | 'hr', |
||
| 180 | 'pre', |
||
| 181 | 'code', |
||
| 182 | 'blockquote', |
||
| 183 | 'table', |
||
| 184 | 'tr', |
||
| 185 | 'td', |
||
| 186 | 'th', |
||
| 187 | 'thead', |
||
| 188 | 'tbody', |
||
| 189 | 'strong', |
||
| 190 | 'em', |
||
| 191 | 'b', |
||
| 192 | 'i', |
||
| 193 | 'u', |
||
| 194 | 's', |
||
| 195 | 'span', |
||
| 196 | 'a', |
||
| 197 | 'p', |
||
| 198 | 'br', |
||
| 199 | 'nobr', |
||
| 200 | 'ul', |
||
| 201 | 'ol', |
||
| 202 | 'li', |
||
| 203 | 'img', |
||
| 204 | ]); |
||
| 205 | } |
||
| 206 | |||
| 207 | /** |
||
| 208 | * Renders the previous exception stack for a given Exception. |
||
| 209 | * |
||
| 210 | * @param Throwable $t The exception whose precursors should be rendered. |
||
| 211 | * |
||
| 212 | * @throws Throwable |
||
| 213 | * |
||
| 214 | * @return string HTML content of the rendered previous exceptions. Empty string if there are none. |
||
| 215 | */ |
||
| 216 | 2 | public function renderPreviousExceptions(Throwable $t): string |
|
| 217 | { |
||
| 218 | 2 | $templatePath = $this->defaultTemplatePath . '/_previous-exception.php'; |
|
| 219 | |||
| 220 | 2 | if ($t instanceof CompositeException) { |
|
| 221 | $result = []; |
||
| 222 | foreach ($t->getPreviousExceptions() as $exception) { |
||
| 223 | $result[] = $this->renderTemplate($templatePath, ['throwable' => $exception]); |
||
| 224 | } |
||
| 225 | return implode('', $result); |
||
| 226 | } |
||
| 227 | 2 | if ($t->getPrevious() !== null) { |
|
| 228 | 1 | return $this->renderTemplate($templatePath, ['throwable' => $t->getPrevious()]); |
|
| 229 | } |
||
| 230 | |||
| 231 | 2 | return ''; |
|
| 232 | } |
||
| 233 | |||
| 234 | /** |
||
| 235 | * Renders call stack. |
||
| 236 | * |
||
| 237 | * @param Throwable $t The exception to get call stack from. |
||
| 238 | * |
||
| 239 | * @throws Throwable |
||
| 240 | * |
||
| 241 | * @return string HTML content of the rendered call stack. |
||
| 242 | * |
||
| 243 | * @psalm-param DebugBacktraceType $trace |
||
| 244 | */ |
||
| 245 | 2 | public function renderCallStack(Throwable $t, array $trace = []): string |
|
| 246 | { |
||
| 247 | 2 | $application = $vendor = []; |
|
| 248 | 2 | $application[1] = $this->renderCallStackItem( |
|
| 249 | 2 | $t->getFile(), |
|
| 250 | 2 | $t->getLine(), |
|
| 251 | 2 | null, |
|
| 252 | 2 | null, |
|
| 253 | 2 | [], |
|
| 254 | 2 | 1, |
|
| 255 | 2 | false, |
|
| 256 | 2 | [], |
|
| 257 | 2 | ); |
|
| 258 | |||
| 259 | 2 | $length = count($trace); |
|
| 260 | 2 | for ($i = 0; $i < $length; ++$i) { |
|
| 261 | 1 | $file = !empty($trace[$i]['file']) ? $trace[$i]['file'] : null; |
|
| 262 | 1 | $line = !empty($trace[$i]['line']) ? $trace[$i]['line'] : null; |
|
| 263 | 1 | $class = !empty($trace[$i]['class']) ? $trace[$i]['class'] : null; |
|
| 264 | 1 | $args = !empty($trace[$i]['args']) ? $trace[$i]['args'] : []; |
|
| 265 | |||
| 266 | 1 | $parameters = []; |
|
| 267 | 1 | $function = null; |
|
| 268 | 1 | if (!empty($trace[$i]['function']) && $trace[$i]['function'] !== 'unknown') { |
|
| 269 | 1 | $function = $trace[$i]['function']; |
|
| 270 | 1 | if ($class !== null && !str_contains($function, '{closure}')) { |
|
| 271 | 1 | $parameters = (new \ReflectionMethod($class, $function))->getParameters(); |
|
| 272 | } |
||
| 273 | } |
||
| 274 | 1 | $index = $i + 2; |
|
| 275 | |||
| 276 | 1 | if ($this->isVendorFile($file)) { |
|
| 277 | $vendor[$index] = $this->renderCallStackItem( |
||
| 278 | $file, |
||
| 279 | $line, |
||
| 280 | $class, |
||
| 281 | $function, |
||
| 282 | $args, |
||
| 283 | $index, |
||
| 284 | true, |
||
| 285 | $parameters, |
||
| 286 | ); |
||
| 287 | } else { |
||
| 288 | 1 | $application[$index] = $this->renderCallStackItem( |
|
| 289 | 1 | $file, |
|
| 290 | 1 | $line, |
|
| 291 | 1 | $class, |
|
| 292 | 1 | $function, |
|
| 293 | 1 | $args, |
|
| 294 | 1 | $index, |
|
| 295 | 1 | false, |
|
| 296 | 1 | $parameters, |
|
| 297 | 1 | ); |
|
| 298 | } |
||
| 299 | } |
||
| 300 | |||
| 301 | 2 | return $this->renderTemplate($this->defaultTemplatePath . '/_call-stack-items.php', [ |
|
| 302 | 2 | 'applicationItems' => $application, |
|
| 303 | 2 | 'vendorItemGroups' => $this->groupVendorCallStackItems($vendor), |
|
| 304 | 2 | ]); |
|
| 305 | } |
||
| 306 | |||
| 307 | /** |
||
| 308 | * Converts arguments array to its string representation. |
||
| 309 | * |
||
| 310 | * @param array $args arguments array to be converted |
||
| 311 | * |
||
| 312 | * @return string The string representation of the arguments array. |
||
| 313 | */ |
||
| 314 | 12 | public function argumentsToString(array $args, bool $truncate = true): string |
|
| 315 | { |
||
| 316 | 12 | $count = 0; |
|
| 317 | 12 | $isAssoc = $args !== array_values($args); |
|
| 318 | |||
| 319 | /** |
||
| 320 | * @var mixed $value |
||
| 321 | */ |
||
| 322 | 12 | foreach ($args as $key => $value) { |
|
| 323 | 12 | $count++; |
|
| 324 | |||
| 325 | 12 | if ($truncate && $count >= 5) { |
|
| 326 | 1 | if ($count > 5) { |
|
| 327 | 1 | unset($args[$key]); |
|
| 328 | } else { |
||
| 329 | 1 | $args[$key] = '...'; |
|
| 330 | } |
||
| 331 | 1 | continue; |
|
| 332 | } |
||
| 333 | |||
| 334 | 12 | if (is_object($value)) { |
|
| 335 | 2 | $args[$key] = '<span class="title">' . $this->htmlEncode($this->removeAnonymous($value::class) . '#' . spl_object_id($value)) . '</span>'; |
|
| 336 | 11 | } elseif (is_bool($value)) { |
|
| 337 | 2 | $args[$key] = '<span class="keyword">' . ($value ? 'true' : 'false') . '</span>'; |
|
| 338 | 10 | } elseif (is_string($value)) { |
|
| 339 | 6 | $fullValue = $this->htmlEncode($value); |
|
| 340 | 6 | if ($truncate && mb_strlen($value, 'UTF-8') > 32) { |
|
| 341 | 2 | $displayValue = $this->htmlEncode(mb_substr($value, 0, 32, 'UTF-8')) . '...'; |
|
| 342 | 2 | $args[$key] = "<span class=\"string\" title=\"$fullValue\">'$displayValue'</span>"; |
|
| 343 | } else { |
||
| 344 | 6 | $args[$key] = "<span class=\"string\">'$fullValue'</span>"; |
|
| 345 | } |
||
| 346 | 6 | } elseif (is_array($value)) { |
|
| 347 | 2 | unset($args[$key]); |
|
| 348 | 2 | $args[$key] = '[' . $this->argumentsToString($value, $truncate) . ']'; |
|
| 349 | 5 | } elseif ($value === null) { |
|
| 350 | 2 | $args[$key] = '<span class="keyword">null</span>'; |
|
| 351 | 4 | } elseif (is_resource($value)) { |
|
| 352 | 1 | $args[$key] = '<span class="keyword">resource</span>'; |
|
| 353 | } else { |
||
| 354 | 3 | $args[$key] = '<span class="number">' . (string) $value . '</span>'; |
|
| 355 | } |
||
| 356 | |||
| 357 | 12 | if (is_string($key)) { |
|
| 358 | 2 | $args[$key] = '<span class="string">\'' . $this->htmlEncode($key) . "'</span> => $args[$key]"; |
|
| 359 | 11 | } elseif ($isAssoc) { |
|
| 360 | 1 | $args[$key] = "<span class=\"number\">$key</span> => $args[$key]"; |
|
| 361 | } |
||
| 362 | } |
||
| 363 | |||
| 364 | /** @var string[] $args */ |
||
| 365 | |||
| 366 | 12 | ksort($args); |
|
| 367 | 12 | return implode(', ', $args); |
|
| 368 | } |
||
| 369 | |||
| 370 | /** |
||
| 371 | * Renders the information about request. |
||
| 372 | * |
||
| 373 | * @return string The rendering result. |
||
| 374 | */ |
||
| 375 | 2 | public function renderRequest(ServerRequestInterface $request): string |
|
| 376 | { |
||
| 377 | 2 | $output = $request->getMethod() . ' ' . $request->getUri() . "\n"; |
|
| 378 | |||
| 379 | 2 | $headers = $request->getHeaders(); |
|
| 380 | 2 | unset($headers['Host']); |
|
| 381 | 2 | ksort($headers); |
|
| 382 | |||
| 383 | 2 | foreach ($headers as $name => $values) { |
|
| 384 | 2 | foreach ($values as $value) { |
|
| 385 | 2 | $output .= "$name: $value\n"; |
|
| 386 | } |
||
| 387 | } |
||
| 388 | |||
| 389 | 2 | $body = (string)$request->getBody(); |
|
| 390 | 2 | if (!empty($body)) { |
|
| 391 | $output .= "\n" . $body . "\n\n"; |
||
| 392 | } |
||
| 393 | |||
| 394 | 2 | return $output; |
|
| 395 | } |
||
| 396 | |||
| 397 | /** |
||
| 398 | * Renders the information about curl request. |
||
| 399 | * |
||
| 400 | * @return string The rendering result. |
||
| 401 | */ |
||
| 402 | 2 | public function renderCurl(ServerRequestInterface $request): string |
|
| 403 | { |
||
| 404 | try { |
||
| 405 | 2 | $output = (new Command()) |
|
| 406 | 2 | ->setRequest($request) |
|
| 407 | 2 | ->build(); |
|
| 408 | 2 | } catch (Throwable $e) { |
|
| 409 | 2 | return 'Error generating curl command: ' . $e->getMessage(); |
|
| 410 | } |
||
| 411 | |||
| 412 | return $output; |
||
| 413 | } |
||
| 414 | |||
| 415 | /** |
||
| 416 | * Creates string containing HTML link which refers to the home page |
||
| 417 | * of determined web-server software and its full name. |
||
| 418 | * |
||
| 419 | * @return string The server software information hyperlink. |
||
| 420 | */ |
||
| 421 | 9 | public function createServerInformationLink(ServerRequestInterface $request): string |
|
| 422 | { |
||
| 423 | 9 | $serverSoftware = (string) ($request->getServerParams()['SERVER_SOFTWARE'] ?? ''); |
|
| 424 | |||
| 425 | 9 | if ($serverSoftware === '') { |
|
| 426 | 2 | return ''; |
|
| 427 | } |
||
| 428 | |||
| 429 | 7 | $serverUrls = [ |
|
| 430 | 7 | 'https://httpd.apache.org/' => ['apache'], |
|
| 431 | 7 | 'https://nginx.org/' => ['nginx'], |
|
| 432 | 7 | 'https://lighttpd.net/' => ['lighttpd'], |
|
| 433 | 7 | 'https://iis.net/' => ['iis', 'services'], |
|
| 434 | 7 | 'https://www.php.net/manual/en/features.commandline.webserver.php' => ['development'], |
|
| 435 | 7 | ]; |
|
| 436 | |||
| 437 | 7 | foreach ($serverUrls as $url => $keywords) { |
|
| 438 | 7 | foreach ($keywords as $keyword) { |
|
| 439 | 7 | if (stripos($serverSoftware, $keyword) !== false) { |
|
| 440 | 6 | return '<a href="' . $url . '" target="_blank" rel="noopener noreferrer">' |
|
| 441 | 6 | . $this->htmlEncode($serverSoftware) . '</a>'; |
|
| 442 | } |
||
| 443 | } |
||
| 444 | } |
||
| 445 | |||
| 446 | 1 | return ''; |
|
| 447 | } |
||
| 448 | |||
| 449 | /** |
||
| 450 | * Returns the name of the throwable instance. |
||
| 451 | * |
||
| 452 | * @return string The name of the throwable instance. |
||
| 453 | */ |
||
| 454 | 2 | public function getThrowableName(Throwable $throwable): string |
|
| 455 | { |
||
| 456 | 2 | $name = $throwable::class; |
|
| 457 | |||
| 458 | 2 | if ($throwable instanceof FriendlyExceptionInterface) { |
|
| 459 | 1 | $name = $throwable->getName() . ' (' . $name . ')'; |
|
| 460 | } |
||
| 461 | |||
| 462 | 2 | return $name; |
|
| 463 | } |
||
| 464 | |||
| 465 | /** |
||
| 466 | * Renders a template. |
||
| 467 | * |
||
| 468 | * @param string $path The full path of the template file for rendering. |
||
| 469 | * @param array $parameters The name-value pairs that will be extracted and made available in the template file. |
||
| 470 | * |
||
| 471 | * @throws Throwable |
||
| 472 | * |
||
| 473 | * @return string The rendering result. |
||
| 474 | * |
||
| 475 | * @psalm-suppress PossiblyInvalidFunctionCall |
||
| 476 | * @psalm-suppress PossiblyFalseArgument |
||
| 477 | * @psalm-suppress UnresolvableInclude |
||
| 478 | */ |
||
| 479 | 10 | private function renderTemplate(string $path, array $parameters): string |
|
| 480 | { |
||
| 481 | 10 | if (!file_exists($path)) { |
|
| 482 | 1 | throw new RuntimeException("Template not found at $path"); |
|
| 483 | } |
||
| 484 | |||
| 485 | 9 | $renderer = function (): void { |
|
| 486 | /** @psalm-suppress MixedArgument */ |
||
| 487 | 9 | extract(func_get_arg(1), EXTR_OVERWRITE); |
|
| 488 | 9 | require func_get_arg(0); |
|
| 489 | 9 | }; |
|
| 490 | |||
| 491 | 9 | $obInitialLevel = ob_get_level(); |
|
| 492 | 9 | ob_start(); |
|
| 493 | 9 | ob_implicit_flush(false); |
|
| 494 | |||
| 495 | try { |
||
| 496 | /** @psalm-suppress PossiblyNullFunctionCall */ |
||
| 497 | 9 | $renderer->bindTo($this)($path, $parameters); |
|
| 498 | 8 | return ob_get_clean(); |
|
| 499 | 1 | } catch (Throwable $e) { |
|
| 500 | 1 | while (ob_get_level() > $obInitialLevel) { |
|
| 501 | 1 | if (!@ob_end_clean()) { |
|
| 502 | ob_clean(); |
||
| 503 | } |
||
| 504 | } |
||
| 505 | 1 | throw $e; |
|
| 506 | } |
||
| 507 | } |
||
| 508 | |||
| 509 | /** |
||
| 510 | * Renders a single call stack element. |
||
| 511 | * |
||
| 512 | * @param string|null $file The name where call has happened. |
||
| 513 | * @param int|null $line The number on which call has happened. |
||
| 514 | * @param string|null $class The called class name. |
||
| 515 | * @param string|null $function The called function/method name. |
||
| 516 | * @param array $args The array of method arguments. |
||
| 517 | * @param int $index The number of the call stack element. |
||
| 518 | * @param bool $isVendorFile Whether given name of the file belongs to the vendor package. |
||
| 519 | * |
||
| 520 | * @throws Throwable |
||
| 521 | * |
||
| 522 | * @return string HTML content of the rendered call stack element. |
||
| 523 | */ |
||
| 524 | 3 | private function renderCallStackItem( |
|
| 525 | ?string $file, |
||
| 526 | ?int $line, |
||
| 527 | ?string $class, |
||
| 528 | ?string $function, |
||
| 529 | array $args, |
||
| 530 | int $index, |
||
| 531 | bool $isVendorFile, |
||
| 532 | array $reflectionParameters, |
||
| 533 | ): string { |
||
| 534 | 3 | $lines = []; |
|
| 535 | 3 | $begin = $end = 0; |
|
| 536 | |||
| 537 | 3 | if ($file !== null && $line !== null) { |
|
| 538 | 3 | $line--; // adjust line number from one-based to zero-based |
|
| 539 | 3 | $lines = @file($file); |
|
| 540 | 3 | if ($line < 0 || $lines === false || ($lineCount = count($lines)) < $line) { |
|
| 541 | 1 | return ''; |
|
| 542 | } |
||
| 543 | 2 | $half = (int) (($index === 1 ? $this->maxSourceLines : $this->maxTraceLines) / 2); |
|
| 544 | 2 | $begin = $line - $half > 0 ? $line - $half : 0; |
|
| 545 | 2 | $end = $line + $half < $lineCount ? $line + $half : $lineCount - 1; |
|
| 546 | } |
||
| 547 | |||
| 548 | 2 | return $this->renderTemplate($this->defaultTemplatePath . '/_call-stack-item.php', [ |
|
| 549 | 2 | 'file' => $file, |
|
| 550 | 2 | 'line' => $line, |
|
| 551 | 2 | 'class' => $class, |
|
| 552 | 2 | 'function' => $function, |
|
| 553 | 2 | 'index' => $index, |
|
| 554 | 2 | 'lines' => $lines, |
|
| 555 | 2 | 'begin' => $begin, |
|
| 556 | 2 | 'end' => $end, |
|
| 557 | 2 | 'args' => $args, |
|
| 558 | 2 | 'isVendorFile' => $isVendorFile, |
|
| 559 | 2 | 'reflectionParameters' => $reflectionParameters, |
|
| 560 | 2 | ]); |
|
| 561 | } |
||
| 562 | |||
| 563 | /** |
||
| 564 | * Groups a vendor call stack items to render. |
||
| 565 | * |
||
| 566 | * @param array<int, string> $items The list of the vendor call stack items. |
||
| 567 | * |
||
| 568 | * @return array<int, array<int, string>> The grouped items of the vendor call stack. |
||
| 569 | */ |
||
| 570 | 3 | private function groupVendorCallStackItems(array $items): array |
|
| 571 | { |
||
| 572 | 3 | $groupIndex = null; |
|
| 573 | 3 | $groupedItems = []; |
|
| 574 | |||
| 575 | 3 | foreach ($items as $index => $item) { |
|
| 576 | 1 | if ($groupIndex === null) { |
|
| 577 | 1 | $groupIndex = $index; |
|
| 578 | 1 | $groupedItems[$groupIndex][$index] = $item; |
|
| 579 | 1 | continue; |
|
| 580 | } |
||
| 581 | |||
| 582 | 1 | if (isset($items[$index - 1])) { |
|
| 583 | 1 | $groupedItems[$groupIndex][$index] = $item; |
|
| 584 | 1 | continue; |
|
| 585 | } |
||
| 586 | |||
| 587 | 1 | $groupIndex = $index; |
|
| 588 | 1 | $groupedItems[$groupIndex][$index] = $item; |
|
| 589 | } |
||
| 590 | |||
| 591 | /** @psalm-var array<int, array<int, string>> $groupedItems It's need for Psalm <=4.30 only. */ |
||
| 592 | |||
| 593 | 3 | return $groupedItems; |
|
| 594 | } |
||
| 595 | |||
| 596 | /** |
||
| 597 | * Determines whether given name of the file belongs to the vendor package. |
||
| 598 | * |
||
| 599 | * @param string|null $file The name to be checked. |
||
| 600 | * |
||
| 601 | * @return bool Whether given name of the file belongs to the vendor package. |
||
| 602 | */ |
||
| 603 | 5 | private function isVendorFile(?string $file): bool |
|
| 604 | { |
||
| 605 | 5 | if ($file === null) { |
|
| 606 | 1 | return false; |
|
| 607 | } |
||
| 608 | |||
| 609 | 4 | $file = realpath($file); |
|
| 610 | |||
| 611 | 4 | if ($file === false) { |
|
| 612 | 1 | return false; |
|
| 613 | } |
||
| 614 | |||
| 615 | 3 | foreach ($this->getVendorPaths() as $vendorPath) { |
|
| 616 | 1 | if (str_starts_with($file, $vendorPath)) { |
|
| 617 | 1 | return true; |
|
| 618 | } |
||
| 619 | } |
||
| 620 | |||
| 621 | 3 | return false; |
|
| 622 | } |
||
| 623 | |||
| 624 | /** |
||
| 625 | * Returns a list of vendor paths. |
||
| 626 | * |
||
| 627 | * @return string[] The list of vendor paths. |
||
| 628 | * |
||
| 629 | * @see $vendorPaths |
||
| 630 | */ |
||
| 631 | 3 | private function getVendorPaths(): array |
|
| 632 | { |
||
| 633 | 3 | if ($this->vendorPaths !== null) { |
|
| 634 | 2 | return $this->vendorPaths; |
|
| 635 | } |
||
| 636 | |||
| 637 | 2 | $rootPath = dirname(__DIR__, 4); |
|
| 638 | |||
| 639 | // If the error handler is installed as a vendor package. |
||
| 640 | /** @psalm-suppress InvalidLiteralArgument It is Psalm bug, {@see https://github.com/vimeo/psalm/issues/9196} */ |
||
| 641 | 2 | if (strlen($rootPath) > 6 && str_contains($rootPath, 'vendor')) { |
|
| 642 | $this->vendorPaths = [$rootPath]; |
||
| 643 | return $this->vendorPaths; |
||
|
0 ignored issues
–
show
Bug
Best Practice
introduced
by
Loading history...
|
|||
| 644 | } |
||
| 645 | |||
| 646 | // If the error handler is installed for development in `yiisoft/yii-dev-tool`. |
||
| 647 | 2 | if (is_file("{$rootPath}/yii-dev") || is_file("{$rootPath}/yii-dev.bat")) { |
|
| 648 | $vendorPaths = glob("{$rootPath}/dev/*/vendor"); |
||
| 649 | /** @var string[] */ |
||
| 650 | $this->vendorPaths = empty($vendorPaths) ? [] : str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $vendorPaths); |
||
| 651 | return $this->vendorPaths; |
||
| 652 | } |
||
| 653 | |||
| 654 | 2 | $this->vendorPaths = []; |
|
| 655 | 2 | return $this->vendorPaths; |
|
|
0 ignored issues
–
show
|
|||
| 656 | } |
||
| 657 | |||
| 658 | 2 | public function removeAnonymous(string $value): string |
|
| 659 | { |
||
| 660 | 2 | $anonymousPosition = strpos($value, '@anonymous'); |
|
| 661 | |||
| 662 | 2 | return $anonymousPosition !== false ? substr($value, 0, $anonymousPosition) : $value; |
|
| 663 | } |
||
| 664 | } |
||
| 665 |