1: | <?php declare(strict_types=1); |
2: | |
3: | namespace Salient\Console; |
4: | |
5: | use Psr\Log\LoggerInterface; |
6: | use Salient\Console\Target\StreamTarget; |
7: | use Salient\Console\ConsoleFormatter as Formatter; |
8: | use Salient\Contract\Console\ConsoleFormatterInterface as FormatterInterface; |
9: | use Salient\Contract\Console\ConsoleInterface; |
10: | use Salient\Contract\Console\ConsoleTargetInterface; |
11: | use Salient\Contract\Console\ConsoleTargetInterface as Target; |
12: | use Salient\Contract\Console\ConsoleTargetPrefixInterface as TargetPrefix; |
13: | use Salient\Contract\Console\ConsoleTargetStreamInterface as TargetStream; |
14: | use Salient\Contract\Console\ConsoleTargetTypeFlag as TargetTypeFlag; |
15: | use Salient\Contract\Core\Exception\Exception; |
16: | use Salient\Contract\Core\Exception\MultipleErrorException; |
17: | use Salient\Contract\Core\Facade\FacadeAwareInterface; |
18: | use Salient\Contract\Core\Unloadable; |
19: | use Salient\Core\Concern\FacadeAwareInstanceTrait; |
20: | use Salient\Utility\Exception\InvalidEnvironmentException; |
21: | use Salient\Utility\Arr; |
22: | use Salient\Utility\Debug; |
23: | use Salient\Utility\Env; |
24: | use Salient\Utility\File; |
25: | use Salient\Utility\Format; |
26: | use Salient\Utility\Get; |
27: | use Salient\Utility\Inflect; |
28: | use Salient\Utility\Str; |
29: | use Salient\Utility\Sys; |
30: | use Throwable; |
31: | |
32: | |
33: | |
34: | |
35: | final class Console implements ConsoleInterface, FacadeAwareInterface, Unloadable |
36: | { |
37: | |
38: | use FacadeAwareInstanceTrait; |
39: | |
40: | private ConsoleState $State; |
41: | |
42: | public function __construct() |
43: | { |
44: | $this->State = new ConsoleState(); |
45: | } |
46: | |
47: | |
48: | |
49: | |
50: | public function getLogger(): LoggerInterface |
51: | { |
52: | return $this->State->Logger ??= new ConsoleLogger($this); |
53: | } |
54: | |
55: | |
56: | |
57: | |
58: | public function unload(): void |
59: | { |
60: | foreach ($this->State->Targets as $target) { |
61: | $this->onlyDeregisterTarget($target); |
62: | } |
63: | $this->closeDeregisteredTargets(); |
64: | } |
65: | |
66: | |
67: | |
68: | |
69: | private function maybeRegisterStdioTargets() |
70: | { |
71: | $output = Env::get('console_target', null); |
72: | |
73: | if ($output !== null) { |
74: | switch (Str::lower($output)) { |
75: | case 'stderr': |
76: | $target = $this->getStderrTarget(); |
77: | |
78: | case 'stdout': |
79: | $target ??= $this->getStdoutTarget(); |
80: | return $this->registerStdioTarget($target); |
81: | |
82: | default: |
83: | throw new InvalidEnvironmentException( |
84: | sprintf('Invalid console_target value: %s', $output) |
85: | ); |
86: | } |
87: | } |
88: | |
89: | if (stream_isatty(\STDERR) && !stream_isatty(\STDOUT)) { |
90: | return $this->registerStderrTarget(); |
91: | } |
92: | |
93: | return $this->registerStdioTargets(); |
94: | } |
95: | |
96: | |
97: | |
98: | |
99: | public function registerStdioTargets() |
100: | { |
101: | if (\PHP_SAPI !== 'cli') { |
102: | return $this; |
103: | } |
104: | |
105: | $stderr = $this->getStderrTarget(); |
106: | $stderrLevels = self::LEVELS_ERRORS_AND_WARNINGS; |
107: | |
108: | $stdout = $this->getStdoutTarget(); |
109: | $stdoutLevels = Env::getDebug() |
110: | ? self::LEVELS_INFO |
111: | : self::LEVELS_INFO_EXCEPT_DEBUG; |
112: | |
113: | return $this |
114: | ->onlyDeregisterStdioTargets() |
115: | ->registerTarget($stderr, $stderrLevels) |
116: | ->registerTarget($stdout, $stdoutLevels) |
117: | ->closeDeregisteredTargets(); |
118: | } |
119: | |
120: | |
121: | |
122: | |
123: | public function registerStderrTarget() |
124: | { |
125: | if (\PHP_SAPI !== 'cli') { |
126: | return $this; |
127: | } |
128: | |
129: | return $this->registerStdioTarget($this->getStderrTarget()); |
130: | } |
131: | |
132: | |
133: | |
134: | |
135: | private function registerStdioTarget(Target $target) |
136: | { |
137: | $levels = Env::getDebug() |
138: | ? self::LEVELS_ALL |
139: | : self::LEVELS_ALL_EXCEPT_DEBUG; |
140: | |
141: | return $this |
142: | ->onlyDeregisterStdioTargets() |
143: | ->registerTarget($target, $levels) |
144: | ->closeDeregisteredTargets(); |
145: | } |
146: | |
147: | |
148: | |
149: | |
150: | private function onlyDeregisterStdioTargets() |
151: | { |
152: | if (!$this->State->StdioTargetsByLevel) { |
153: | return $this; |
154: | } |
155: | $targets = $this->reduceTargets($this->State->StdioTargetsByLevel); |
156: | foreach ($targets as $target) { |
157: | $this->onlyDeregisterTarget($target); |
158: | } |
159: | return $this; |
160: | } |
161: | |
162: | |
163: | |
164: | |
165: | public function registerTarget( |
166: | Target $target, |
167: | array $levels = Console::LEVELS_ALL |
168: | ) { |
169: | $type = 0; |
170: | |
171: | if ($target instanceof TargetStream) { |
172: | $type |= TargetTypeFlag::STREAM; |
173: | |
174: | if ($target->isStdout()) { |
175: | $type |= TargetTypeFlag::STDIO | TargetTypeFlag::STDOUT; |
176: | $this->State->StdoutTarget = $target; |
177: | } |
178: | |
179: | if ($target->isStderr()) { |
180: | $type |= TargetTypeFlag::STDIO | TargetTypeFlag::STDERR; |
181: | $this->State->StderrTarget = $target; |
182: | } |
183: | |
184: | if ($type & TargetTypeFlag::STDIO) { |
185: | $targetsByLevel[] = &$this->State->StdioTargetsByLevel; |
186: | } |
187: | |
188: | if ($target->isTty()) { |
189: | $type |= TargetTypeFlag::TTY; |
190: | $targetsByLevel[] = &$this->State->TtyTargetsByLevel; |
191: | } |
192: | } |
193: | |
194: | $targetsByLevel[] = &$this->State->TargetsByLevel; |
195: | |
196: | $targetId = spl_object_id($target); |
197: | |
198: | foreach ($targetsByLevel as &$targetsByLevel) { |
199: | foreach ($levels as $level) { |
200: | $targetsByLevel[$level][$targetId] = $target; |
201: | } |
202: | } |
203: | |
204: | $this->State->Targets[$targetId] = $target; |
205: | $this->State->TargetTypeFlags[$targetId] = $type; |
206: | |
207: | return $this; |
208: | } |
209: | |
210: | |
211: | |
212: | |
213: | public function deregisterTarget(Target $target) |
214: | { |
215: | return $this->onlyDeregisterTarget($target)->closeDeregisteredTargets(); |
216: | } |
217: | |
218: | |
219: | |
220: | |
221: | private function onlyDeregisterTarget(Target $target) |
222: | { |
223: | $targetId = spl_object_id($target); |
224: | |
225: | unset($this->State->Targets[$targetId]); |
226: | |
227: | foreach ([ |
228: | &$this->State->TargetsByLevel, |
229: | &$this->State->TtyTargetsByLevel, |
230: | &$this->State->StdioTargetsByLevel, |
231: | ] as &$targetsByLevel) { |
232: | foreach ($targetsByLevel as $level => &$targets) { |
233: | unset($targets[$targetId]); |
234: | if (!$targets) { |
235: | unset($targetsByLevel[$level]); |
236: | } |
237: | } |
238: | } |
239: | |
240: | if ($this->State->StderrTarget === $target) { |
241: | $this->State->StderrTarget = null; |
242: | } |
243: | |
244: | if ($this->State->StdoutTarget === $target) { |
245: | $this->State->StdoutTarget = null; |
246: | } |
247: | |
248: | $this->State->DeregisteredTargets[$targetId] = $target; |
249: | |
250: | |
251: | if ( |
252: | $this->State->Targets |
253: | && (!$this->State->StdoutTarget || !$this->State->StderrTarget) |
254: | ) { |
255: | foreach (array_reverse($this->State->Targets) as $target) { |
256: | if (!$target instanceof TargetStream) { |
257: | continue; |
258: | } |
259: | if (!$this->State->StdoutTarget && $target->isStdout()) { |
260: | $this->State->StdoutTarget = $target; |
261: | if ($this->State->StderrTarget) { |
262: | break; |
263: | } |
264: | } |
265: | if (!$this->State->StderrTarget && $target->isStderr()) { |
266: | $this->State->StderrTarget = $target; |
267: | if ($this->State->StdoutTarget) { |
268: | break; |
269: | } |
270: | } |
271: | } |
272: | } |
273: | |
274: | return $this; |
275: | } |
276: | |
277: | |
278: | |
279: | |
280: | private function closeDeregisteredTargets() |
281: | { |
282: | |
283: | |
284: | $this->State->DeregisteredTargets = array_diff_key( |
285: | $this->State->DeregisteredTargets, |
286: | $this->State->Targets, |
287: | ); |
288: | foreach ($this->State->DeregisteredTargets as $i => $target) { |
289: | $target->close(); |
290: | unset($this->State->TargetTypeFlags[$i]); |
291: | unset($this->State->DeregisteredTargets[$i]); |
292: | } |
293: | return $this; |
294: | } |
295: | |
296: | |
297: | |
298: | |
299: | public function getTargets(?int $level = null, int $flags = 0): array |
300: | { |
301: | $targets = $level === null |
302: | ? $this->State->Targets |
303: | : $this->State->TargetsByLevel[$level] ?? []; |
304: | if ($flags) { |
305: | $targets = $this->filterTargets($targets, $flags); |
306: | } |
307: | return array_values($targets); |
308: | } |
309: | |
310: | |
311: | |
312: | |
313: | public function setTargetPrefix(?string $prefix, int $flags = 0) |
314: | { |
315: | foreach ($this->filterTargets($this->State->Targets, $flags) as $target) { |
316: | if ($target instanceof TargetPrefix) { |
317: | $target->setPrefix($prefix); |
318: | } |
319: | } |
320: | return $this; |
321: | } |
322: | |
323: | |
324: | |
325: | |
326: | public function getWidth(int $level = Console::LEVEL_INFO): ?int |
327: | { |
328: | return $this->maybeGetTtyTarget($level)->getWidth(); |
329: | } |
330: | |
331: | |
332: | |
333: | |
334: | public function getFormatter(int $level = Console::LEVEL_INFO): FormatterInterface |
335: | { |
336: | return $this->maybeGetTtyTarget($level)->getFormatter(); |
337: | } |
338: | |
339: | |
340: | |
341: | |
342: | private function maybeGetTtyTarget(int $level): TargetStream |
343: | { |
344: | |
345: | $targets = $this->State->TtyTargetsByLevel[$level] |
346: | ?? $this->State->StdioTargetsByLevel[$level] |
347: | ?? $this->State->TargetsByLevel[$level] |
348: | ?? []; |
349: | |
350: | $target = reset($targets); |
351: | if (!$target || !$target instanceof TargetStream) { |
352: | $target = $this->getStderrTarget(); |
353: | if (!$target->isTty()) { |
354: | return $this->getStdoutTarget(); |
355: | } |
356: | } |
357: | |
358: | return $target; |
359: | } |
360: | |
361: | |
362: | |
363: | |
364: | public function getStdoutTarget(): TargetStream |
365: | { |
366: | return $this->State->StdoutTarget ??= StreamTarget::fromStream(\STDOUT); |
367: | } |
368: | |
369: | |
370: | |
371: | |
372: | public function getStderrTarget(): TargetStream |
373: | { |
374: | return $this->State->StderrTarget ??= StreamTarget::fromStream(\STDERR); |
375: | } |
376: | |
377: | |
378: | |
379: | |
380: | public function getErrorCount(): int |
381: | { |
382: | return $this->State->ErrorCount; |
383: | } |
384: | |
385: | |
386: | |
387: | |
388: | public function getWarningCount(): int |
389: | { |
390: | return $this->State->WarningCount; |
391: | } |
392: | |
393: | |
394: | |
395: | |
396: | public function escape(string $string): string |
397: | { |
398: | return Formatter::escapeTags($string); |
399: | } |
400: | |
401: | |
402: | |
403: | |
404: | public function summary( |
405: | string $finishedText = 'Command finished', |
406: | string $successText = 'without errors', |
407: | bool $withResourceUsage = false, |
408: | bool $withoutErrorCount = false, |
409: | bool $withStandardMessageType = false |
410: | ) { |
411: | if ($withResourceUsage) { |
412: | |
413: | $requestTime = $_SERVER['REQUEST_TIME_FLOAT']; |
414: | $usage = sprintf( |
415: | 'in %.3fs (%s memory used)', |
416: | microtime(true) - $requestTime, |
417: | Format::bytes(memory_get_peak_usage()), |
418: | ); |
419: | } |
420: | |
421: | $msg1 = rtrim($finishedText); |
422: | $errors = $this->State->ErrorCount; |
423: | $warnings = $this->State->WarningCount; |
424: | if ( |
425: | (!$errors && !$warnings) |
426: | |
427: | |
428: | || ($withoutErrorCount && $successText === '') |
429: | ) { |
430: | return $this->write( |
431: | self::LEVEL_INFO, |
432: | Arr::implode(' ', [$msg1, $successText, $usage ?? null], ''), |
433: | null, |
434: | $withStandardMessageType |
435: | ? self::TYPE_SUMMARY |
436: | : self::TYPE_SUCCESS, |
437: | ); |
438: | } |
439: | |
440: | if (!$withoutErrorCount) { |
441: | $msg2 = 'with ' . Inflect::format($errors, '{{#}} {{#:error}}'); |
442: | if ($warnings) { |
443: | $msg2 .= ' and ' . Inflect::format($warnings, '{{#}} {{#:warning}}'); |
444: | } |
445: | } |
446: | |
447: | return $this->write( |
448: | $withoutErrorCount || $withStandardMessageType |
449: | ? self::LEVEL_INFO |
450: | : ($errors ? self::LEVEL_ERROR : self::LEVEL_WARNING), |
451: | Arr::implode(' ', [$msg1, $msg2 ?? null, $usage ?? null], ''), |
452: | null, |
453: | $withoutErrorCount || $withStandardMessageType |
454: | ? self::TYPE_SUMMARY |
455: | : self::TYPE_FAILURE, |
456: | ); |
457: | } |
458: | |
459: | |
460: | |
461: | |
462: | public function print( |
463: | string $msg, |
464: | int $level = Console::LEVEL_INFO, |
465: | int $type = Console::TYPE_UNFORMATTED |
466: | ) { |
467: | return $this->_write($level, $msg, null, $type, null, $this->State->TargetsByLevel); |
468: | } |
469: | |
470: | |
471: | |
472: | |
473: | public function printOut( |
474: | string $msg, |
475: | int $level = Console::LEVEL_INFO, |
476: | int $type = Console::TYPE_UNFORMATTED |
477: | ) { |
478: | return $this->_write($level, $msg, null, $type, null, $this->State->StdioTargetsByLevel); |
479: | } |
480: | |
481: | |
482: | |
483: | |
484: | public function printTty( |
485: | string $msg, |
486: | int $level = Console::LEVEL_INFO, |
487: | int $type = Console::TYPE_UNFORMATTED |
488: | ) { |
489: | return $this->_write($level, $msg, null, $type, null, $this->State->TtyTargetsByLevel); |
490: | } |
491: | |
492: | |
493: | |
494: | |
495: | public function printStdout( |
496: | string $msg, |
497: | int $level = Console::LEVEL_INFO, |
498: | int $type = Console::TYPE_UNFORMATTED |
499: | ) { |
500: | $targets = [$level => [$this->getStdoutTarget()]]; |
501: | return $this->_write($level, $msg, null, $type, null, $targets); |
502: | } |
503: | |
504: | |
505: | |
506: | |
507: | public function printStderr( |
508: | string $msg, |
509: | int $level = Console::LEVEL_INFO, |
510: | int $type = Console::TYPE_UNFORMATTED |
511: | ) { |
512: | $targets = [$level => [$this->getStderrTarget()]]; |
513: | return $this->_write($level, $msg, null, $type, null, $targets); |
514: | } |
515: | |
516: | |
517: | |
518: | |
519: | public function message( |
520: | string $msg1, |
521: | ?string $msg2 = null, |
522: | int $level = Console::LEVEL_INFO, |
523: | int $type = Console::TYPE_UNDECORATED, |
524: | ?Throwable $ex = null, |
525: | bool $count = true |
526: | ) { |
527: | if ($count) { |
528: | $this->count($level); |
529: | } |
530: | return $this->write($level, $msg1, $msg2, $type, $ex); |
531: | } |
532: | |
533: | |
534: | |
535: | |
536: | public function messageOnce( |
537: | string $msg1, |
538: | ?string $msg2 = null, |
539: | int $level = Console::LEVEL_INFO, |
540: | int $type = Console::TYPE_UNDECORATED, |
541: | ?Throwable $ex = null, |
542: | bool $count = true |
543: | ) { |
544: | if ($count) { |
545: | $this->count($level); |
546: | } |
547: | return $this->writeOnce($level, $msg1, $msg2, $type, $ex); |
548: | } |
549: | |
550: | |
551: | |
552: | |
553: | public function count($level) |
554: | { |
555: | switch ($level) { |
556: | case self::LEVEL_EMERGENCY: |
557: | case self::LEVEL_ALERT: |
558: | case self::LEVEL_CRITICAL: |
559: | case self::LEVEL_ERROR: |
560: | $this->State->ErrorCount++; |
561: | break; |
562: | |
563: | case self::LEVEL_WARNING: |
564: | $this->State->WarningCount++; |
565: | break; |
566: | } |
567: | |
568: | return $this; |
569: | } |
570: | |
571: | |
572: | |
573: | |
574: | public function error( |
575: | string $msg1, |
576: | ?string $msg2 = null, |
577: | ?Throwable $ex = null, |
578: | bool $count = true |
579: | ) { |
580: | !$count || $this->State->ErrorCount++; |
581: | |
582: | return $this->write(self::LEVEL_ERROR, $msg1, $msg2, self::TYPE_STANDARD, $ex); |
583: | } |
584: | |
585: | |
586: | |
587: | |
588: | public function errorOnce( |
589: | string $msg1, |
590: | ?string $msg2 = null, |
591: | ?Throwable $ex = null, |
592: | bool $count = true |
593: | ) { |
594: | !$count || $this->State->ErrorCount++; |
595: | |
596: | return $this->writeOnce(self::LEVEL_ERROR, $msg1, $msg2, self::TYPE_STANDARD, $ex); |
597: | } |
598: | |
599: | |
600: | |
601: | |
602: | public function warn( |
603: | string $msg1, |
604: | ?string $msg2 = null, |
605: | ?Throwable $ex = null, |
606: | bool $count = true |
607: | ) { |
608: | !$count || $this->State->WarningCount++; |
609: | |
610: | return $this->write(self::LEVEL_WARNING, $msg1, $msg2, self::TYPE_STANDARD, $ex); |
611: | } |
612: | |
613: | |
614: | |
615: | |
616: | public function warnOnce( |
617: | string $msg1, |
618: | ?string $msg2 = null, |
619: | ?Throwable $ex = null, |
620: | bool $count = true |
621: | ) { |
622: | !$count || $this->State->WarningCount++; |
623: | |
624: | return $this->writeOnce(self::LEVEL_WARNING, $msg1, $msg2, self::TYPE_STANDARD, $ex); |
625: | } |
626: | |
627: | |
628: | |
629: | |
630: | public function info( |
631: | string $msg1, |
632: | ?string $msg2 = null |
633: | ) { |
634: | return $this->write(self::LEVEL_NOTICE, $msg1, $msg2); |
635: | } |
636: | |
637: | |
638: | |
639: | |
640: | public function infoOnce( |
641: | string $msg1, |
642: | ?string $msg2 = null |
643: | ) { |
644: | return $this->writeOnce(self::LEVEL_NOTICE, $msg1, $msg2); |
645: | } |
646: | |
647: | |
648: | |
649: | |
650: | public function log( |
651: | string $msg1, |
652: | ?string $msg2 = null |
653: | ) { |
654: | return $this->write(self::LEVEL_INFO, $msg1, $msg2); |
655: | } |
656: | |
657: | |
658: | |
659: | |
660: | public function logOnce( |
661: | string $msg1, |
662: | ?string $msg2 = null |
663: | ) { |
664: | return $this->writeOnce(self::LEVEL_INFO, $msg1, $msg2); |
665: | } |
666: | |
667: | |
668: | |
669: | |
670: | public function logProgress( |
671: | string $msg1, |
672: | ?string $msg2 = null |
673: | ) { |
674: | if (!($this->State->TtyTargetsByLevel[self::LEVEL_INFO] ?? null)) { |
675: | return $this; |
676: | } |
677: | |
678: | if ($msg2 === null || $msg2 === '') { |
679: | $msg1 = rtrim($msg1, "\r") . "\r"; |
680: | } else { |
681: | $msg2 = rtrim($msg2, "\r") . "\r"; |
682: | } |
683: | |
684: | return $this->writeTty(self::LEVEL_INFO, $msg1, $msg2, self::TYPE_PROGRESS); |
685: | } |
686: | |
687: | |
688: | |
689: | |
690: | public function clearProgress() |
691: | { |
692: | if (!($this->State->TtyTargetsByLevel[self::LEVEL_INFO] ?? null)) { |
693: | return $this; |
694: | } |
695: | |
696: | return $this->writeTty(self::LEVEL_INFO, "\r", null, self::TYPE_UNFORMATTED); |
697: | } |
698: | |
699: | |
700: | |
701: | |
702: | public function debug( |
703: | string $msg1, |
704: | ?string $msg2 = null, |
705: | ?Throwable $ex = null, |
706: | int $depth = 0 |
707: | ) { |
708: | if ($this->Facade !== null) { |
709: | $depth++; |
710: | } |
711: | |
712: | $caller = implode('', Debug::getCaller($depth)); |
713: | $msg1 = $msg1 ? ' __' . $msg1 . '__' : ''; |
714: | |
715: | return $this->write(self::LEVEL_DEBUG, "{{$caller}}{$msg1}", $msg2, self::TYPE_STANDARD, $ex); |
716: | } |
717: | |
718: | |
719: | |
720: | |
721: | public function debugOnce( |
722: | string $msg1, |
723: | ?string $msg2 = null, |
724: | ?Throwable $ex = null, |
725: | int $depth = 0 |
726: | ) { |
727: | if ($this->Facade !== null) { |
728: | $depth++; |
729: | } |
730: | |
731: | $caller = implode('', Debug::getCaller($depth)); |
732: | |
733: | return $this->writeOnce(self::LEVEL_DEBUG, "{{$caller}} __" . $msg1 . '__', $msg2, self::TYPE_STANDARD, $ex); |
734: | } |
735: | |
736: | |
737: | |
738: | |
739: | public function group( |
740: | string $msg1, |
741: | ?string $msg2 = null, |
742: | ?string $endMsg1 = null, |
743: | ?string $endMsg2 = null |
744: | ) { |
745: | $this->State->GroupLevel++; |
746: | $this->State->GroupMessageStack[] = [$endMsg1, $endMsg1 === null ? null : $endMsg2]; |
747: | return $this->write(self::LEVEL_NOTICE, $msg1, $msg2, self::TYPE_GROUP_START); |
748: | } |
749: | |
750: | |
751: | |
752: | |
753: | public function groupEnd() |
754: | { |
755: | [$msg1, $msg2] = array_pop($this->State->GroupMessageStack) ?? [null, null]; |
756: | if ($msg1 !== null) { |
757: | $this->write(self::LEVEL_NOTICE, $msg1, $msg2, self::TYPE_GROUP_END); |
758: | } |
759: | if ($this->State->LastWritten !== [__METHOD__, '']) { |
760: | $this->printOut('', self::LEVEL_NOTICE); |
761: | $this->State->LastWritten = [__METHOD__, '']; |
762: | } |
763: | if ($this->State->GroupLevel > -1) { |
764: | $this->State->GroupLevel--; |
765: | } |
766: | return $this; |
767: | } |
768: | |
769: | |
770: | |
771: | |
772: | public function exception( |
773: | Throwable $exception, |
774: | int $level = Console::LEVEL_ERROR, |
775: | ?int $traceLevel = Console::LEVEL_DEBUG |
776: | ) { |
777: | $ex = $exception; |
778: | $msg2 = ''; |
779: | $i = 0; |
780: | do { |
781: | if ($i++) { |
782: | $class = $this->escape(Get::basename(get_class($ex))); |
783: | $msg2 .= sprintf("\nCaused by __%s__: ", $class); |
784: | } |
785: | |
786: | if ( |
787: | $ex instanceof MultipleErrorException |
788: | && !$ex->hasUnreportedErrors() |
789: | ) { |
790: | $message = $this->escape($ex->getMessageOnly()); |
791: | } else { |
792: | $message = $this->escape($ex->getMessage()); |
793: | } |
794: | |
795: | if ($level <= self::LEVEL_ERROR || ($debug ??= Env::getDebug())) { |
796: | $file = $this->escape($ex->getFile()); |
797: | $line = $ex->getLine(); |
798: | $msg2 .= sprintf('%s ~~in %s:%d~~', $message, $file, $line); |
799: | } else { |
800: | $msg2 .= $message; |
801: | } |
802: | } while ($ex = $ex->getPrevious()); |
803: | |
804: | $class = $this->escape(Get::basename(get_class($exception))); |
805: | $this->count($level)->write( |
806: | $level, |
807: | "{$class}:", |
808: | $msg2, |
809: | self::TYPE_STANDARD, |
810: | $exception, |
811: | true |
812: | ); |
813: | if ($traceLevel === null) { |
814: | return $this; |
815: | } |
816: | $this->write( |
817: | $traceLevel, |
818: | 'Stack trace:', |
819: | "\n" . $exception->getTraceAsString() |
820: | ); |
821: | if ($exception instanceof Exception) { |
822: | foreach ($exception->getMetadata() as $key => $value) { |
823: | $value = rtrim((string) $value, "\n"); |
824: | $this->write($traceLevel, "{$key}:", "\n{$value}"); |
825: | } |
826: | } |
827: | |
828: | return $this; |
829: | } |
830: | |
831: | |
832: | |
833: | |
834: | |
835: | private function reduceTargets(array $targets): array |
836: | { |
837: | foreach ($targets as $levelTargets) { |
838: | foreach ($levelTargets as $target) { |
839: | $targetId = spl_object_id($target); |
840: | $reduced[$targetId] = $target; |
841: | } |
842: | } |
843: | return $reduced ?? []; |
844: | } |
845: | |
846: | |
847: | |
848: | |
849: | |
850: | |
851: | |
852: | |
853: | private function write( |
854: | int $level, |
855: | string $msg1, |
856: | ?string $msg2, |
857: | int $type = self::TYPE_STANDARD, |
858: | ?Throwable $ex = null, |
859: | bool $msg2HasTags = false |
860: | ) { |
861: | return $this->_write($level, $msg1, $msg2, $type, $ex, $this->State->TargetsByLevel, $msg2HasTags); |
862: | } |
863: | |
864: | |
865: | |
866: | |
867: | |
868: | |
869: | |
870: | |
871: | private function writeOnce( |
872: | int $level, |
873: | string $msg1, |
874: | ?string $msg2, |
875: | int $type = self::TYPE_STANDARD, |
876: | ?Throwable $ex = null, |
877: | bool $msg2HasTags = false |
878: | ) { |
879: | $hash = Get::hash(implode("\0", [$level, $msg1, $msg2, $type, $msg2HasTags])); |
880: | if (isset($this->State->Written[$hash])) { |
881: | return $this; |
882: | } |
883: | $this->State->Written[$hash] = true; |
884: | return $this->_write($level, $msg1, $msg2, $type, $ex, $this->State->TargetsByLevel, $msg2HasTags); |
885: | } |
886: | |
887: | |
888: | |
889: | |
890: | |
891: | |
892: | |
893: | |
894: | private function writeTty( |
895: | int $level, |
896: | string $msg1, |
897: | ?string $msg2, |
898: | int $type = self::TYPE_STANDARD, |
899: | ?Throwable $ex = null, |
900: | bool $msg2HasTags = false |
901: | ) { |
902: | return $this->_write($level, $msg1, $msg2, $type, $ex, $this->State->TtyTargetsByLevel, $msg2HasTags); |
903: | } |
904: | |
905: | |
906: | |
907: | |
908: | |
909: | |
910: | |
911: | |
912: | |
913: | private function _write( |
914: | int $level, |
915: | string $msg1, |
916: | ?string $msg2, |
917: | int $type, |
918: | ?Throwable $ex, |
919: | array &$targets, |
920: | bool $msg2HasTags = false |
921: | ) { |
922: | if (!$this->State->Targets) { |
923: | $logTarget = StreamTarget::fromPath(sprintf( |
924: | '%s/%s-%s-%s.log', |
925: | Sys::getTempDir(), |
926: | Sys::getProgramBasename(), |
927: | Get::hash(File::realpath(Sys::getProgramName())), |
928: | Sys::getUserId(), |
929: | )); |
930: | $this->registerTarget($logTarget, self::LEVELS_ALL); |
931: | $this->maybeRegisterStdioTargets(); |
932: | } |
933: | |
934: | |
935: | if ($ex) { |
936: | $context['exception'] = $ex; |
937: | } |
938: | |
939: | $margin = max(0, $this->State->GroupLevel * 2); |
940: | |
941: | foreach ($targets[$level] ?? [] as $target) { |
942: | $formatter = $target->getFormatter(); |
943: | |
944: | $indent = mb_strlen($formatter->getMessagePrefix($level, $type)); |
945: | $indent = max(0, strpos($msg1, "\n") !== false ? $indent : $indent - 4); |
946: | |
947: | $_msg1 = $msg1 === '' ? '' : $formatter->format($msg1); |
948: | |
949: | if ($margin + $indent > 0 && strpos($msg1, "\n") !== false) { |
950: | $_msg1 = str_replace("\n", "\n" . str_repeat(' ', $margin + $indent), $_msg1); |
951: | } |
952: | |
953: | $_msg2 = null; |
954: | if ($msg2 !== null && $msg2 !== '') { |
955: | $_msg2 = $msg2HasTags ? $formatter->format($msg2) : $msg2; |
956: | $_msg2 = strpos($msg2, "\n") !== false |
957: | ? str_replace("\n", "\n" . str_repeat(' ', $margin + $indent + 2), "\n" . ltrim($_msg2)) |
958: | : ($_msg1 !== '' ? ' ' : '') . $_msg2; |
959: | } |
960: | |
961: | if ($type === self::TYPE_PROGRESS) { |
962: | $formatter = $formatter->withSpinnerState($this->State->SpinnerState); |
963: | } |
964: | |
965: | $message = $formatter->formatMessage($_msg1, $_msg2, $level, $type); |
966: | $target->write($level, str_repeat(' ', $margin) . $message, $context ?? []); |
967: | } |
968: | |
969: | $this->State->LastWritten = []; |
970: | |
971: | return $this; |
972: | } |
973: | |
974: | |
975: | |
976: | |
977: | |
978: | |
979: | private function filterTargets(array $targets, int $flags): array |
980: | { |
981: | $invert = false; |
982: | if ($flags & TargetTypeFlag::INVERT) { |
983: | $flags &= ~TargetTypeFlag::INVERT; |
984: | $invert = true; |
985: | } |
986: | foreach ($targets as $targetId => $target) { |
987: | if (($flags === 0 && !$invert) || ( |
988: | $this->State->TargetTypeFlags[$targetId] & $flags |
989: | xor $invert |
990: | )) { |
991: | $filtered[$targetId] = $target; |
992: | } |
993: | } |
994: | return $filtered ?? []; |
995: | } |
996: | } |
997: | |
998: | |
999: | |
1000: | |
1001: | final class ConsoleState |
1002: | { |
1003: | |
1004: | public array $StdioTargetsByLevel = []; |
1005: | |
1006: | public array $TtyTargetsByLevel = []; |
1007: | |
1008: | public array $TargetsByLevel = []; |
1009: | |
1010: | public array $Targets = []; |
1011: | |
1012: | public array $DeregisteredTargets = []; |
1013: | |
1014: | public array $TargetTypeFlags = []; |
1015: | public ?TargetStream $StdoutTarget = null; |
1016: | public ?TargetStream $StderrTarget = null; |
1017: | public int $GroupLevel = -1; |
1018: | |
1019: | public array $GroupMessageStack = []; |
1020: | public int $ErrorCount = 0; |
1021: | public int $WarningCount = 0; |
1022: | |
1023: | public array $Written = []; |
1024: | |
1025: | public array $LastWritten = []; |
1026: | |
1027: | public ?array $SpinnerState; |
1028: | public LoggerInterface $Logger; |
1029: | |
1030: | private function __clone() {} |
1031: | } |
1032: | |