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\ExceptionInterface;
18: use Salient\Contract\Core\FacadeAwareInterface;
19: use Salient\Contract\Core\FacadeInterface;
20: use Salient\Contract\Core\MessageLevel as Level;
21: use Salient\Contract\Core\MessageLevelGroup as LevelGroup;
22: use Salient\Contract\Core\MultipleErrorExceptionInterface;
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: * Logs messages to registered targets
40: *
41: * {@see ConsoleWriter} methods should generally be called via the
42: * {@see Console} facade. If a {@see ConsoleWriter} instance is required, call
43: * {@see Console::getInstance()}.
44: *
45: * @implements FacadeAwareInterface<FacadeInterface<self>>
46: */
47: final class ConsoleWriter implements ConsoleWriterInterface, FacadeAwareInterface, Unloadable
48: {
49: /** @use HasFacade<FacadeInterface<self>> */
50: use HasFacade;
51:
52: private ConsoleWriterState $State;
53:
54: public function __construct()
55: {
56: $this->State = new ConsoleWriterState();
57: }
58:
59: /**
60: * @inheritDoc
61: */
62: public function getLogger(): LoggerInterface
63: {
64: return $this->State->Logger ??= new ConsoleLogger($this);
65: }
66:
67: /**
68: * @inheritDoc
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: * @return $this
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: // No break
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: * @inheritDoc
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: * @inheritDoc
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: * @return $this
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: * @return $this
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: * @inheritDoc
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: * @inheritDoc
224: */
225: public function deregisterTarget(Target $target)
226: {
227: return $this->onlyDeregisterTarget($target)->closeDeregisteredTargets();
228: }
229:
230: /**
231: * @return $this
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: // Reinstate previous STDOUT and STDERR targets if possible
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: * @return $this
291: */
292: private function closeDeregisteredTargets()
293: {
294: // Reduce `$this->State->DeregisteredTargets` to targets not
295: // subsequently re-registered
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
337: */
338: public function getWidth(int $level = Level::INFO): ?int
339: {
340: return $this->maybeGetTtyTarget($level)->getWidth();
341: }
342:
343: /**
344: * @inheritDoc
345: */
346: public function getFormatter(int $level = Level::INFO): FormatterInterface
347: {
348: return $this->maybeGetTtyTarget($level)->getFormatter();
349: }
350:
351: /**
352: * @param Level::* $level
353: */
354: private function maybeGetTtyTarget(int $level): TargetStream
355: {
356: /** @var Target[] */
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: * @inheritDoc
375: */
376: public function getStdoutTarget(): TargetStream
377: {
378: return $this->State->StdoutTarget ??= StreamTarget::fromStream(\STDOUT);
379: }
380:
381: /**
382: * @inheritDoc
383: */
384: public function getStderrTarget(): TargetStream
385: {
386: return $this->State->StderrTarget ??= StreamTarget::fromStream(\STDERR);
387: }
388:
389: /**
390: * @inheritDoc
391: */
392: public function getErrorCount(): int
393: {
394: return $this->State->ErrorCount;
395: }
396:
397: /**
398: * @inheritDoc
399: */
400: public function getWarningCount(): int
401: {
402: return $this->State->WarningCount;
403: }
404:
405: /**
406: * @inheritDoc
407: */
408: public function escape(string $string): string
409: {
410: return Formatter::escapeTags($string);
411: }
412:
413: /**
414: * @inheritDoc
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: // If output is identical for success and failure, print a
437: // success message
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: /** @var int&Level::* $level */
541: return $this->write($level, $msg1, $msg2, $type, $ex);
542: }
543:
544: /**
545: * @inheritDoc
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: /** @var int&Level::* $level */
559: return $this->writeOnce($level, $msg1, $msg2, $type, $ex);
560: }
561:
562: /**
563: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: $file = $this->escape($ex->getFile());
806: $line = $ex->getLine();
807: $msg2 .= sprintf('%s ~~in %s:%d~~', $message, $file, $line);
808: } while ($ex = $ex->getPrevious());
809:
810: $class = $this->escape(Get::basename(get_class($exception)));
811: $this->count($level)->write(
812: $level,
813: sprintf('__%s__:', $class),
814: $msg2,
815: MessageType::STANDARD,
816: $exception,
817: true
818: );
819: if ($traceLevel === null) {
820: return $this;
821: }
822: $this->write(
823: $traceLevel,
824: '__Stack trace:__',
825: "\n" . $exception->getTraceAsString()
826: );
827: if ($exception instanceof ExceptionInterface) {
828: foreach ($exception->getMetadata() as $key => $value) {
829: $value = rtrim((string) $value, "\n");
830: $this->write($traceLevel, "__{$key}:__", "\n{$value}");
831: }
832: }
833:
834: return $this;
835: }
836:
837: /**
838: * @param array<Level::*,Target[]> $targets
839: * @return Target[]
840: */
841: private function reduceTargets(array $targets): array
842: {
843: foreach ($targets as $levelTargets) {
844: foreach ($levelTargets as $target) {
845: $targetId = spl_object_id($target);
846: $reduced[$targetId] = $target;
847: }
848: }
849: return $reduced ?? [];
850: }
851:
852: /**
853: * Send a message to registered targets
854: *
855: * @param int&Level::* $level
856: * @param MessageType::* $type
857: * @return $this
858: */
859: private function write(
860: int $level,
861: string $msg1,
862: ?string $msg2,
863: int $type = MessageType::STANDARD,
864: ?Throwable $ex = null,
865: bool $msg2HasTags = false
866: ) {
867: return $this->_write($level, $msg1, $msg2, $type, $ex, $this->State->TargetsByLevel, $msg2HasTags);
868: }
869:
870: /**
871: * Send a message to registered targets once per run
872: *
873: * @param int&Level::* $level
874: * @param MessageType::* $type
875: * @return $this
876: */
877: private function writeOnce(
878: int $level,
879: string $msg1,
880: ?string $msg2,
881: int $type = MessageType::STANDARD,
882: ?Throwable $ex = null,
883: bool $msg2HasTags = false
884: ) {
885: $hash = Get::hash(implode("\0", [$level, $msg1, $msg2, $type, $msg2HasTags]));
886: if (isset($this->State->Written[$hash])) {
887: return $this;
888: }
889: $this->State->Written[$hash] = true;
890: return $this->_write($level, $msg1, $msg2, $type, $ex, $this->State->TargetsByLevel, $msg2HasTags);
891: }
892:
893: /**
894: * Send a message to registered TTY targets
895: *
896: * @param int&Level::* $level
897: * @param MessageType::* $type
898: * @return $this
899: */
900: private function writeTty(
901: int $level,
902: string $msg1,
903: ?string $msg2,
904: int $type = MessageType::STANDARD,
905: ?Throwable $ex = null,
906: bool $msg2HasTags = false
907: ) {
908: return $this->_write($level, $msg1, $msg2, $type, $ex, $this->State->TtyTargetsByLevel, $msg2HasTags);
909: }
910:
911: /**
912: * @param Level::* $level
913: * @param MessageType::* $type
914: * @param array<Level::*,Target[]> $targets
915: * @return $this
916: */
917: private function _write(
918: int $level,
919: string $msg1,
920: ?string $msg2,
921: int $type,
922: ?Throwable $ex,
923: array &$targets,
924: bool $msg2HasTags = false
925: ) {
926: if (!$this->State->Targets) {
927: $logTarget = StreamTarget::fromPath(sprintf(
928: '%s/%s-%s-%s.log',
929: Sys::getTempDir(),
930: Sys::getProgramBasename(),
931: Get::hash(File::realpath(Sys::getProgramName())),
932: Sys::getUserId(),
933: ));
934: $this->registerTarget($logTarget, LevelGroup::ALL);
935: $this->maybeRegisterStdioTargets();
936: }
937:
938: // As per PSR-3 Section 1.3
939: if ($ex) {
940: $context['exception'] = $ex;
941: }
942:
943: $margin = max(0, $this->State->GroupLevel * 2);
944:
945: foreach ($targets[$level] ?? [] as $target) {
946: $formatter = $target->getFormatter();
947:
948: $indent = mb_strlen($formatter->getMessagePrefix($level, $type));
949: $indent = max(0, strpos($msg1, "\n") !== false ? $indent : $indent - 4);
950:
951: $_msg1 = $msg1 === '' ? '' : $formatter->format($msg1);
952:
953: if ($margin + $indent > 0 && strpos($msg1, "\n") !== false) {
954: $_msg1 = str_replace("\n", "\n" . str_repeat(' ', $margin + $indent), $_msg1);
955: }
956:
957: $_msg2 = null;
958: if ($msg2 !== null && $msg2 !== '') {
959: $_msg2 = $msg2HasTags ? $formatter->format($msg2) : $msg2;
960: $_msg2 = strpos($msg2, "\n") !== false
961: ? str_replace("\n", "\n" . str_repeat(' ', $margin + $indent + 2), "\n" . ltrim($_msg2))
962: : ($_msg1 !== '' ? ' ' : '') . $_msg2;
963: }
964:
965: if ($type === MessageType::PROGRESS) {
966: $formatter = $formatter->withSpinnerState($this->State->SpinnerState);
967: }
968:
969: $message = $formatter->formatMessage($_msg1, $_msg2, $level, $type);
970: $target->write($level, str_repeat(' ', $margin) . $message, $context ?? []);
971: }
972:
973: $this->State->LastWritten = [];
974:
975: return $this;
976: }
977:
978: /**
979: * @param array<int,ConsoleTargetInterface> $targets
980: * @param int-mask-of<TargetTypeFlag::*> $flags
981: * @return array<int,ConsoleTargetInterface>
982: */
983: private function filterTargets(array $targets, int $flags): array
984: {
985: $invert = false;
986: if ($flags & TargetTypeFlag::INVERT) {
987: $flags &= ~TargetTypeFlag::INVERT;
988: $invert = true;
989: }
990: foreach ($targets as $targetId => $target) {
991: if (($flags === 0 && !$invert) || (
992: $this->State->TargetTypeFlags[$targetId] & $flags
993: xor $invert
994: )) {
995: $filtered[$targetId] = $target;
996: }
997: }
998: return $filtered ?? [];
999: }
1000: }
1001: