| 1: | <?php declare(strict_types=1); |
| 2: | |
| 3: | namespace Salient\Console; |
| 4: | |
| 5: | use Psr\Log\LoggerInterface as PsrLoggerInterface; |
| 6: | use Salient\Console\Target\StreamTarget; |
| 7: | use Salient\Contract\Console\Target\HasPrefix; |
| 8: | use Salient\Contract\Console\Target\StreamTargetInterface; |
| 9: | use Salient\Contract\Console\Target\TargetInterface; |
| 10: | use Salient\Contract\Console\ConsoleInterface; |
| 11: | use Salient\Contract\Core\Exception\Exception; |
| 12: | use Salient\Contract\Core\Exception\MultipleErrorException; |
| 13: | use Salient\Contract\Core\Facade\FacadeAwareInterface; |
| 14: | use Salient\Contract\Core\Unloadable; |
| 15: | use Salient\Core\Concern\FacadeAwareInstanceTrait; |
| 16: | use Salient\Utility\Arr; |
| 17: | use Salient\Utility\Debug; |
| 18: | use Salient\Utility\Env; |
| 19: | use Salient\Utility\File; |
| 20: | use Salient\Utility\Format; |
| 21: | use Salient\Utility\Get; |
| 22: | use Salient\Utility\Inflect; |
| 23: | use Salient\Utility\Sys; |
| 24: | use Throwable; |
| 25: | |
| 26: | |
| 27: | |
| 28: | |
| 29: | |
| 30: | |
| 31: | class Console implements ConsoleInterface, FacadeAwareInterface, Unloadable |
| 32: | { |
| 33: | |
| 34: | use FacadeAwareInstanceTrait; |
| 35: | |
| 36: | private ConsoleState $State; |
| 37: | |
| 38: | |
| 39: | |
| 40: | |
| 41: | public function __construct() |
| 42: | { |
| 43: | $this->State = new ConsoleState(); |
| 44: | } |
| 45: | |
| 46: | |
| 47: | |
| 48: | |
| 49: | public function unload(): void |
| 50: | { |
| 51: | foreach ($this->State->Targets as $target) { |
| 52: | $this->deregisterTarget($target); |
| 53: | } |
| 54: | } |
| 55: | |
| 56: | |
| 57: | |
| 58: | |
| 59: | public function logger(): PsrLoggerInterface |
| 60: | { |
| 61: | return $this->State->Logger ??= |
| 62: | new ConsoleLogger($this->getReturnable()); |
| 63: | } |
| 64: | |
| 65: | |
| 66: | |
| 67: | |
| 68: | public function registerTarget( |
| 69: | TargetInterface $target, |
| 70: | array $levels = Console::LEVELS_ALL |
| 71: | ) { |
| 72: | $id = spl_object_id($target); |
| 73: | $register = function (array &$byLevel) use ($target, $levels, $id) { |
| 74: | foreach ($levels as $level) { |
| 75: | $byLevel[$level][$id] = $target; |
| 76: | } |
| 77: | }; |
| 78: | $flags = 0; |
| 79: | if ($target instanceof StreamTargetInterface) { |
| 80: | if ($target->isStdout()) { |
| 81: | $flags |= self::TARGET_STDIO | self::TARGET_STDOUT; |
| 82: | $this->State->StdoutTarget = $target; |
| 83: | } |
| 84: | if ($target->isStderr()) { |
| 85: | $flags |= self::TARGET_STDIO | self::TARGET_STDERR; |
| 86: | $this->State->StderrTarget = $target; |
| 87: | } |
| 88: | if ($flags) { |
| 89: | $register($this->State->StdioTargetsByLevel); |
| 90: | } |
| 91: | if ($target->isTty()) { |
| 92: | $flags |= self::TARGET_TTY; |
| 93: | $register($this->State->TtyTargetsByLevel); |
| 94: | } |
| 95: | $flags |= self::TARGET_STREAM; |
| 96: | } |
| 97: | $register($this->State->TargetsByLevel); |
| 98: | $this->State->Targets[$id] = $target; |
| 99: | $this->State->TargetFlags[$id] = $flags; |
| 100: | return $this; |
| 101: | } |
| 102: | |
| 103: | |
| 104: | |
| 105: | |
| 106: | public function deregisterTarget(TargetInterface $target) |
| 107: | { |
| 108: | $id = spl_object_id($target); |
| 109: | $deregister = function (array &$byLevel) use ($id) { |
| 110: | foreach (array_keys($byLevel) as $level) { |
| 111: | unset($byLevel[$level][$id]); |
| 112: | if (!$byLevel[$level]) { |
| 113: | unset($byLevel[$level]); |
| 114: | } |
| 115: | } |
| 116: | }; |
| 117: | unset($this->State->Targets[$id]); |
| 118: | unset($this->State->TargetFlags[$id]); |
| 119: | $deregister($this->State->TargetsByLevel); |
| 120: | $deregister($this->State->TtyTargetsByLevel); |
| 121: | $deregister($this->State->StdioTargetsByLevel); |
| 122: | if ($target instanceof StreamTargetInterface) { |
| 123: | if ($target === $this->State->StderrTarget) { |
| 124: | $this->State->StderrTarget = Arr::last($this->filterTargets(self::TARGET_STDERR)); |
| 125: | } |
| 126: | if ($target === $this->State->StdoutTarget) { |
| 127: | $this->State->StdoutTarget = Arr::last($this->filterTargets(self::TARGET_STDOUT)); |
| 128: | } |
| 129: | } |
| 130: | return $this; |
| 131: | } |
| 132: | |
| 133: | |
| 134: | |
| 135: | |
| 136: | public function registerStderrTarget(?bool $debug = null) |
| 137: | { |
| 138: | if (\PHP_SAPI !== 'cli') { |
| 139: | return $this; |
| 140: | } |
| 141: | $debug ??= Env::getDebug(); |
| 142: | $stderr = $this->getStderrTarget(); |
| 143: | return $this |
| 144: | ->deregisterStdioTargets() |
| 145: | ->registerTarget($stderr, $debug ? self::LEVELS_ALL : self::LEVELS_ALL_EXCEPT_DEBUG); |
| 146: | } |
| 147: | |
| 148: | |
| 149: | |
| 150: | |
| 151: | public function registerStdioTargets(?bool $debug = null) |
| 152: | { |
| 153: | if (\PHP_SAPI !== 'cli') { |
| 154: | return $this; |
| 155: | } |
| 156: | $debug ??= Env::getDebug(); |
| 157: | $stderr = $this->getStderrTarget(); |
| 158: | $stdout = $this->getStdoutTarget(); |
| 159: | return $this |
| 160: | ->deregisterStdioTargets() |
| 161: | ->registerTarget($stderr, self::LEVELS_ERRORS_AND_WARNINGS) |
| 162: | ->registerTarget($stdout, $debug ? self::LEVELS_INFO : self::LEVELS_INFO_EXCEPT_DEBUG); |
| 163: | } |
| 164: | |
| 165: | |
| 166: | |
| 167: | |
| 168: | public function setPrefix(?string $prefix, int $targetFlags = 0) |
| 169: | { |
| 170: | foreach ($this->filterTargets($targetFlags) as $target) { |
| 171: | if ($target instanceof HasPrefix) { |
| 172: | $target->setPrefix($prefix); |
| 173: | } |
| 174: | } |
| 175: | return $this; |
| 176: | } |
| 177: | |
| 178: | |
| 179: | |
| 180: | |
| 181: | public function getTargets(?int $level = null, int $targetFlags = 0): array |
| 182: | { |
| 183: | $targets = $level === null |
| 184: | ? $this->State->Targets |
| 185: | : $this->State->TargetsByLevel[$level] ?? []; |
| 186: | return array_values($targetFlags |
| 187: | ? $this->filterTargets($targetFlags, $targets) |
| 188: | : $targets); |
| 189: | } |
| 190: | |
| 191: | |
| 192: | |
| 193: | |
| 194: | public function getStdoutTarget(): StreamTargetInterface |
| 195: | { |
| 196: | return $this->State->StdoutTarget ??= |
| 197: | new StreamTarget(\STDOUT); |
| 198: | } |
| 199: | |
| 200: | |
| 201: | |
| 202: | |
| 203: | public function getStderrTarget(): StreamTargetInterface |
| 204: | { |
| 205: | return $this->State->StderrTarget ??= |
| 206: | new StreamTarget(\STDERR); |
| 207: | } |
| 208: | |
| 209: | |
| 210: | |
| 211: | |
| 212: | public function getTtyTarget(): StreamTargetInterface |
| 213: | { |
| 214: | return ($stderr = $this->getStderrTarget())->isTty() |
| 215: | ? $stderr |
| 216: | : (($stdout = $this->getStdoutTarget())->isTty() |
| 217: | ? $stdout |
| 218: | : $stderr); |
| 219: | } |
| 220: | |
| 221: | |
| 222: | |
| 223: | |
| 224: | public function escape(string $string, bool $escapeNewlines = false): string |
| 225: | { |
| 226: | return ConsoleUtil::escape($string, $escapeNewlines); |
| 227: | } |
| 228: | |
| 229: | |
| 230: | |
| 231: | |
| 232: | public function removeEscapes(string $string): string |
| 233: | { |
| 234: | return ConsoleUtil::removeEscapes($string); |
| 235: | } |
| 236: | |
| 237: | |
| 238: | |
| 239: | |
| 240: | public function removeTags(string $string): string |
| 241: | { |
| 242: | return ConsoleUtil::removeTags($string); |
| 243: | } |
| 244: | |
| 245: | |
| 246: | |
| 247: | |
| 248: | public function error(string $msg1, ?string $msg2 = null, ?Throwable $ex = null, bool $count = true) |
| 249: | { |
| 250: | return $this->write(self::LEVEL_ERROR, $msg1, $msg2, false, $ex, $count); |
| 251: | } |
| 252: | |
| 253: | |
| 254: | |
| 255: | |
| 256: | public function errorOnce(string $msg1, ?string $msg2 = null, ?Throwable $ex = null, bool $count = true) |
| 257: | { |
| 258: | return $this->write(self::LEVEL_ERROR, $msg1, $msg2, true, $ex, $count); |
| 259: | } |
| 260: | |
| 261: | |
| 262: | |
| 263: | |
| 264: | public function warn(string $msg1, ?string $msg2 = null, ?Throwable $ex = null, bool $count = true) |
| 265: | { |
| 266: | return $this->write(self::LEVEL_WARNING, $msg1, $msg2, false, $ex, $count); |
| 267: | } |
| 268: | |
| 269: | |
| 270: | |
| 271: | |
| 272: | public function warnOnce(string $msg1, ?string $msg2 = null, ?Throwable $ex = null, bool $count = true) |
| 273: | { |
| 274: | return $this->write(self::LEVEL_WARNING, $msg1, $msg2, true, $ex, $count); |
| 275: | } |
| 276: | |
| 277: | |
| 278: | |
| 279: | |
| 280: | public function group(string $msg1, ?string $msg2 = null, ?string $endMsg1 = null, ?string $endMsg2 = null) |
| 281: | { |
| 282: | $this->State->Groups++; |
| 283: | $this->State->GroupMessages[] = [$endMsg1 ?? ($endMsg2 === null ? null : ''), $endMsg2]; |
| 284: | return $this->write(self::LEVEL_NOTICE, $msg1, $msg2, false, null, true, self::TYPE_GROUP_START); |
| 285: | } |
| 286: | |
| 287: | |
| 288: | |
| 289: | |
| 290: | public function groupEnd() |
| 291: | { |
| 292: | [$msg1, $msg2] = array_pop($this->State->GroupMessages) ?? [null, null]; |
| 293: | if ($msg1 !== null) { |
| 294: | $this->write(self::LEVEL_NOTICE, $msg1, $msg2, false, null, true, self::TYPE_GROUP_END); |
| 295: | } |
| 296: | if ( |
| 297: | !$this->State->LastWriteWasEmptyGroupEnd |
| 298: | && ($targets = $this->getTargets(self::LEVEL_NOTICE, self::TARGET_STDIO | self::TARGET_TTY)) |
| 299: | ) { |
| 300: | $targets = [self::LEVEL_NOTICE => $targets]; |
| 301: | $this->write(self::LEVEL_NOTICE, '', null, false, null, false, self::TYPE_UNFORMATTED, $targets); |
| 302: | $this->State->LastWriteWasEmptyGroupEnd = true; |
| 303: | } |
| 304: | if ($this->State->Groups > -1) { |
| 305: | $this->State->Groups--; |
| 306: | } |
| 307: | return $this; |
| 308: | } |
| 309: | |
| 310: | |
| 311: | |
| 312: | |
| 313: | public function info(string $msg1, ?string $msg2 = null) |
| 314: | { |
| 315: | return $this->write(self::LEVEL_NOTICE, $msg1, $msg2); |
| 316: | } |
| 317: | |
| 318: | |
| 319: | |
| 320: | |
| 321: | public function infoOnce(string $msg1, ?string $msg2 = null) |
| 322: | { |
| 323: | return $this->write(self::LEVEL_NOTICE, $msg1, $msg2, true); |
| 324: | } |
| 325: | |
| 326: | |
| 327: | |
| 328: | |
| 329: | public function log(string $msg1, ?string $msg2 = null) |
| 330: | { |
| 331: | return $this->write(self::LEVEL_INFO, $msg1, $msg2); |
| 332: | } |
| 333: | |
| 334: | |
| 335: | |
| 336: | |
| 337: | public function logOnce(string $msg1, ?string $msg2 = null) |
| 338: | { |
| 339: | return $this->write(self::LEVEL_INFO, $msg1, $msg2, true); |
| 340: | } |
| 341: | |
| 342: | |
| 343: | |
| 344: | |
| 345: | public function logProgress(string $msg1, ?string $msg2 = null) |
| 346: | { |
| 347: | if ($msg2 === null || $msg2 === '') { |
| 348: | $msg1 = rtrim($msg1, "\r") . "\r"; |
| 349: | } else { |
| 350: | $msg2 = rtrim($msg2, "\r") . "\r"; |
| 351: | } |
| 352: | return $this->write(self::LEVEL_INFO, $msg1, $msg2, false, null, false, self::TYPE_PROGRESS, $this->State->TtyTargetsByLevel); |
| 353: | } |
| 354: | |
| 355: | |
| 356: | |
| 357: | |
| 358: | public function clearProgress() |
| 359: | { |
| 360: | return $this->write(self::LEVEL_INFO, "\r", null, false, null, false, self::TYPE_UNFORMATTED, $this->State->TtyTargetsByLevel); |
| 361: | } |
| 362: | |
| 363: | |
| 364: | |
| 365: | |
| 366: | public function debug(string $msg1, ?string $msg2 = null, ?Throwable $ex = null, int $depth = 0) |
| 367: | { |
| 368: | return $this->doDebug($msg1, $msg2, $ex, $depth); |
| 369: | } |
| 370: | |
| 371: | |
| 372: | |
| 373: | |
| 374: | public function debugOnce(string $msg1, ?string $msg2 = null, ?Throwable $ex = null, int $depth = 0) |
| 375: | { |
| 376: | return $this->doDebug($msg1, $msg2, $ex, $depth, true); |
| 377: | } |
| 378: | |
| 379: | |
| 380: | |
| 381: | |
| 382: | private function doDebug(string $msg1, ?string $msg2, ?Throwable $ex, int $depth, bool $once = false) |
| 383: | { |
| 384: | $this->Facade === null || $depth++; |
| 385: | if ($msg1 !== '') { |
| 386: | $msg1 = " __{$msg1}__"; |
| 387: | } |
| 388: | $msg1 = '{' . implode('', Debug::getCaller($depth + 1)) . '}' . $msg1; |
| 389: | return $this->write(self::LEVEL_DEBUG, $msg1, $msg2, $once, $ex); |
| 390: | } |
| 391: | |
| 392: | |
| 393: | |
| 394: | |
| 395: | public function message( |
| 396: | string $msg1, |
| 397: | ?string $msg2 = null, |
| 398: | int $level = Console::LEVEL_INFO, |
| 399: | int $type = Console::TYPE_UNDECORATED, |
| 400: | ?Throwable $ex = null, |
| 401: | bool $count = true |
| 402: | ) { |
| 403: | return $this->write($level, $msg1, $msg2, false, $ex, $count, $type); |
| 404: | } |
| 405: | |
| 406: | |
| 407: | |
| 408: | |
| 409: | public function messageOnce( |
| 410: | string $msg1, |
| 411: | ?string $msg2 = null, |
| 412: | int $level = Console::LEVEL_INFO, |
| 413: | int $type = Console::TYPE_UNDECORATED, |
| 414: | ?Throwable $ex = null, |
| 415: | bool $count = true |
| 416: | ) { |
| 417: | return $this->write($level, $msg1, $msg2, true, $ex, $count, $type); |
| 418: | } |
| 419: | |
| 420: | |
| 421: | |
| 422: | |
| 423: | public function exception( |
| 424: | Throwable $exception, |
| 425: | int $level = Console::LEVEL_ERROR, |
| 426: | ?int $traceLevel = Console::LEVEL_DEBUG, |
| 427: | bool $count = true |
| 428: | ) { |
| 429: | $addLine = $level <= self::LEVEL_ERROR || Env::getDebug(); |
| 430: | $msg1 = $this->escape(Get::basename(get_class($exception))) . ':'; |
| 431: | $ex = $exception; |
| 432: | $msg2 = ''; |
| 433: | do { |
| 434: | if ($ex !== $exception) { |
| 435: | $msg2 .= sprintf( |
| 436: | "\nCaused by __%s__: ", |
| 437: | $this->escape(Get::basename(get_class($ex))), |
| 438: | ); |
| 439: | } |
| 440: | $msg2 .= $this->escape( |
| 441: | $ex instanceof MultipleErrorException && !$ex->hasUnreportedErrors() |
| 442: | ? $ex->getMessageOnly() |
| 443: | : $ex->getMessage() |
| 444: | ); |
| 445: | if ($addLine) { |
| 446: | $msg2 .= sprintf( |
| 447: | ' ~~in %s:%d~~', |
| 448: | $this->escape($ex->getFile()), |
| 449: | $ex->getLine(), |
| 450: | ); |
| 451: | } |
| 452: | } while ($ex = $ex->getPrevious()); |
| 453: | |
| 454: | $this->State->Msg2HasTags = true; |
| 455: | try { |
| 456: | $this->write($level, $msg1, $msg2, false, $exception, $count); |
| 457: | } finally { |
| 458: | $this->State->Msg2HasTags = false; |
| 459: | } |
| 460: | |
| 461: | if ($traceLevel !== null) { |
| 462: | $this->write($traceLevel, 'Stack trace:', "\n" . $exception->getTraceAsString()); |
| 463: | if ($exception instanceof Exception) { |
| 464: | foreach ($exception->getMetadata() as $key => $value) { |
| 465: | $this->write($traceLevel, $key . ':', "\n" . rtrim((string) $value, "\n")); |
| 466: | } |
| 467: | } |
| 468: | } |
| 469: | |
| 470: | return $this; |
| 471: | } |
| 472: | |
| 473: | |
| 474: | |
| 475: | |
| 476: | public function summary( |
| 477: | string $finishedText = 'Command finished', |
| 478: | string $successText = 'without errors', |
| 479: | bool $withResourceUsage = false, |
| 480: | bool $withoutErrorsAndWarnings = false, |
| 481: | bool $withGenericType = false |
| 482: | ) { |
| 483: | $errors = $this->State->Errors; |
| 484: | $warnings = $this->State->Warnings; |
| 485: | $hasErrors = $errors || $warnings; |
| 486: | $msg[] = rtrim($finishedText); |
| 487: | if (!$hasErrors) { |
| 488: | $msg[] = $successText; |
| 489: | } elseif (!$withoutErrorsAndWarnings) { |
| 490: | $msg[] = 'with ' . Inflect::format($errors, '{{#}} {{#:error}}') |
| 491: | . ($warnings |
| 492: | ? ' and ' . Inflect::format($warnings, '{{#}} {{#:warning}}') |
| 493: | : ''); |
| 494: | } |
| 495: | if ($withResourceUsage) { |
| 496: | |
| 497: | $requestTime = $_SERVER['REQUEST_TIME_FLOAT']; |
| 498: | $msg[] = sprintf( |
| 499: | 'in %.3fs (%s memory used)', |
| 500: | microtime(true) - $requestTime, |
| 501: | Format::bytes(memory_get_peak_usage()), |
| 502: | ); |
| 503: | } |
| 504: | |
| 505: | return $this->write( |
| 506: | !$hasErrors || $withoutErrorsAndWarnings || $withGenericType |
| 507: | ? self::LEVEL_INFO |
| 508: | : ($errors ? self::LEVEL_ERROR : self::LEVEL_WARNING), |
| 509: | Arr::implode(' ', $msg, ''), |
| 510: | null, |
| 511: | false, |
| 512: | null, |
| 513: | false, |
| 514: | ($hasErrors && $withoutErrorsAndWarnings) || $withGenericType |
| 515: | ? self::TYPE_SUMMARY |
| 516: | : ($hasErrors ? self::TYPE_FAILURE : self::TYPE_SUCCESS), |
| 517: | ); |
| 518: | } |
| 519: | |
| 520: | |
| 521: | |
| 522: | |
| 523: | public function print(string $msg, int $level = Console::LEVEL_INFO) |
| 524: | { |
| 525: | return $this->write($level, $msg, null, false, null, false, self::TYPE_UNFORMATTED); |
| 526: | } |
| 527: | |
| 528: | |
| 529: | |
| 530: | |
| 531: | public function printStdio(string $msg, int $level = Console::LEVEL_INFO) |
| 532: | { |
| 533: | return $this->write($level, $msg, null, false, null, false, self::TYPE_UNFORMATTED, $this->State->StdioTargetsByLevel); |
| 534: | } |
| 535: | |
| 536: | |
| 537: | |
| 538: | |
| 539: | public function printTty(string $msg, int $level = Console::LEVEL_INFO) |
| 540: | { |
| 541: | return $this->write($level, $msg, null, false, null, false, self::TYPE_UNFORMATTED, $this->State->TtyTargetsByLevel); |
| 542: | } |
| 543: | |
| 544: | |
| 545: | |
| 546: | |
| 547: | public function printStdout(string $msg, int $level = Console::LEVEL_INFO) |
| 548: | { |
| 549: | $targets = [$level => [$this->getStdoutTarget()]]; |
| 550: | return $this->write($level, $msg, null, false, null, false, self::TYPE_UNFORMATTED, $targets); |
| 551: | } |
| 552: | |
| 553: | |
| 554: | |
| 555: | |
| 556: | public function count(int $level) |
| 557: | { |
| 558: | if ($level <= self::LEVEL_ERROR) { |
| 559: | $this->State->Errors++; |
| 560: | } elseif ($level === self::LEVEL_WARNING) { |
| 561: | $this->State->Warnings++; |
| 562: | } |
| 563: | return $this; |
| 564: | } |
| 565: | |
| 566: | |
| 567: | |
| 568: | |
| 569: | public function errors(): int |
| 570: | { |
| 571: | return $this->State->Errors; |
| 572: | } |
| 573: | |
| 574: | |
| 575: | |
| 576: | |
| 577: | public function warnings(): int |
| 578: | { |
| 579: | return $this->State->Warnings; |
| 580: | } |
| 581: | |
| 582: | |
| 583: | |
| 584: | |
| 585: | protected function deregisterStdioTargets() |
| 586: | { |
| 587: | foreach ($this->filterTargets(self::TARGET_STDIO) as $target) { |
| 588: | $this->deregisterTarget($target); |
| 589: | } |
| 590: | return $this; |
| 591: | } |
| 592: | |
| 593: | |
| 594: | |
| 595: | |
| 596: | |
| 597: | |
| 598: | protected function filterTargets(int $flags, ?array $targets = null): array |
| 599: | { |
| 600: | $targets ??= $this->State->Targets; |
| 601: | $invert = false; |
| 602: | if ($flags & self::TARGET_INVERT) { |
| 603: | $flags &= ~self::TARGET_INVERT; |
| 604: | $invert = true; |
| 605: | } |
| 606: | if (!$flags) { |
| 607: | return $targets; |
| 608: | } |
| 609: | foreach ($targets as $id => $target) { |
| 610: | if ( |
| 611: | $this->State->TargetFlags[$id] & $flags |
| 612: | xor $invert |
| 613: | ) { |
| 614: | $filtered[$id] = $target; |
| 615: | } |
| 616: | } |
| 617: | return $filtered ?? []; |
| 618: | } |
| 619: | |
| 620: | |
| 621: | |
| 622: | |
| 623: | |
| 624: | |
| 625: | |
| 626: | |
| 627: | |
| 628: | |
| 629: | protected function write( |
| 630: | int $level, |
| 631: | string $msg1, |
| 632: | ?string $msg2, |
| 633: | bool $once = false, |
| 634: | ?Throwable $ex = null, |
| 635: | bool $count = true, |
| 636: | int $type = self::TYPE_STANDARD, |
| 637: | ?array &$targets = null |
| 638: | ) { |
| 639: | if ($count && $level <= self::LEVEL_WARNING) { |
| 640: | $this->count($level); |
| 641: | } |
| 642: | |
| 643: | if ($once) { |
| 644: | $hash = Get::hash(implode("\0", [$level, $msg1, $msg2, $type])); |
| 645: | if (isset($this->State->Written[$hash])) { |
| 646: | return $this; |
| 647: | } |
| 648: | $this->State->Written[$hash] = true; |
| 649: | } |
| 650: | |
| 651: | if (!$this->State->Targets && !$targets) { |
| 652: | $this->registerStderrTarget(); |
| 653: | $this->registerTarget(StreamTarget::fromFile(sprintf( |
| 654: | '%s/%s-%s-%s.log', |
| 655: | Sys::getTempDir(), |
| 656: | Sys::getProgramBasename(), |
| 657: | Get::hash(File::realpath(Sys::getProgramName())), |
| 658: | Sys::getUserId(), |
| 659: | )), Env::getDebug() ? self::LEVELS_ALL : self::LEVELS_ALL_EXCEPT_DEBUG); |
| 660: | } |
| 661: | |
| 662: | $this->State->LastWriteWasEmptyGroupEnd = false; |
| 663: | |
| 664: | if ($msg2 === '') { |
| 665: | $msg2 = null; |
| 666: | $msg2HasNewline = false; |
| 667: | } else { |
| 668: | $msg2HasNewline = $msg2 !== null && strpos($msg2, "\n") !== false; |
| 669: | if ($msg2HasNewline) { |
| 670: | $msg2 = "\n" . ltrim($msg2); |
| 671: | } |
| 672: | } |
| 673: | |
| 674: | |
| 675: | |
| 676: | $context = $ex ? ['exception' => $ex] : []; |
| 677: | $groupIndent = max(0, $this->State->Groups * 2); |
| 678: | $msg1HasNewline = $msg1 !== '' && strpos($msg1, "\n") !== false; |
| 679: | $_targets = $targets ?? $this->State->TargetsByLevel; |
| 680: | foreach ($_targets[$level] ?? [] as $target) { |
| 681: | $formatter = $target->getFormatter(); |
| 682: | $prefixWidth = mb_strlen($formatter->getMessagePrefix($level, $type)); |
| 683: | $indent = $groupIndent + ( |
| 684: | $msg1HasNewline || $prefixWidth < 4 |
| 685: | ? $prefixWidth |
| 686: | : $prefixWidth - 4 |
| 687: | ); |
| 688: | $_msg1 = $msg1 === '' ? '' : $formatter->format($msg1); |
| 689: | if ($indent && $msg1HasNewline) { |
| 690: | $_msg1 = str_replace("\n", "\n" . str_repeat(' ', $indent), $_msg1); |
| 691: | } |
| 692: | if ($msg2 === null) { |
| 693: | $_msg2 = null; |
| 694: | } else { |
| 695: | $_msg2 = $this->State->Msg2HasTags ? $formatter->format($msg2) : $msg2; |
| 696: | if ($msg2HasNewline) { |
| 697: | $_msg2 = str_replace("\n", "\n" . str_repeat(' ', $indent + 2), $_msg2); |
| 698: | } elseif ($_msg1 !== '') { |
| 699: | $_msg2 = ' ' . $_msg2; |
| 700: | } |
| 701: | } |
| 702: | $message = $formatter->formatMessage($_msg1, $_msg2, $level, $type); |
| 703: | if ($groupIndent && $message !== '') { |
| 704: | $message = str_repeat(' ', $groupIndent) . $message; |
| 705: | } |
| 706: | $target->write($level, $message, $context); |
| 707: | } |
| 708: | |
| 709: | return $this; |
| 710: | } |
| 711: | |
| 712: | |
| 713: | |
| 714: | |
| 715: | |
| 716: | |
| 717: | protected function getReturnable() |
| 718: | { |
| 719: | return $this->Facade === null |
| 720: | ? $this |
| 721: | : $this->withoutFacade($this->Facade, false); |
| 722: | } |
| 723: | } |
| 724: | |
| 725: | |
| 726: | |
| 727: | |
| 728: | final class ConsoleState |
| 729: | { |
| 730: | |
| 731: | public array $Targets = []; |
| 732: | |
| 733: | public array $TargetFlags = []; |
| 734: | |
| 735: | public array $TargetsByLevel = []; |
| 736: | |
| 737: | public array $StdioTargetsByLevel = []; |
| 738: | |
| 739: | public array $TtyTargetsByLevel = []; |
| 740: | public ?StreamTargetInterface $StdoutTarget = null; |
| 741: | public ?StreamTargetInterface $StderrTarget = null; |
| 742: | |
| 743: | public array $Written = []; |
| 744: | public int $Groups = -1; |
| 745: | |
| 746: | public array $GroupMessages = []; |
| 747: | public bool $LastWriteWasEmptyGroupEnd = false; |
| 748: | public bool $Msg2HasTags = false; |
| 749: | public int $Errors = 0; |
| 750: | public int $Warnings = 0; |
| 751: | public PsrLoggerInterface $Logger; |
| 752: | |
| 753: | private function __clone() {} |
| 754: | } |
| 755: | |