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