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: * @implements FacadeAwareInterface<ConsoleInterface>
34: */
35: final class Console implements ConsoleInterface, FacadeAwareInterface, Unloadable
36: {
37: /** @use FacadeAwareInstanceTrait<ConsoleInterface> */
38: use FacadeAwareInstanceTrait;
39:
40: private ConsoleState $State;
41:
42: public function __construct()
43: {
44: $this->State = new ConsoleState();
45: }
46:
47: /**
48: * @inheritDoc
49: */
50: public function getLogger(): LoggerInterface
51: {
52: return $this->State->Logger ??= new ConsoleLogger($this);
53: }
54:
55: /**
56: * @inheritDoc
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: * @return $this
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: // No break
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: * @inheritDoc
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: * @inheritDoc
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: * @return $this
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: * @return $this
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: * @inheritDoc
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: * @inheritDoc
212: */
213: public function deregisterTarget(Target $target)
214: {
215: return $this->onlyDeregisterTarget($target)->closeDeregisteredTargets();
216: }
217:
218: /**
219: * @return $this
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: // Reinstate previous STDOUT and STDERR targets if possible
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: * @return $this
279: */
280: private function closeDeregisteredTargets()
281: {
282: // Reduce `$this->State->DeregisteredTargets` to targets not
283: // subsequently re-registered
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
325: */
326: public function getWidth(int $level = Console::LEVEL_INFO): ?int
327: {
328: return $this->maybeGetTtyTarget($level)->getWidth();
329: }
330:
331: /**
332: * @inheritDoc
333: */
334: public function getFormatter(int $level = Console::LEVEL_INFO): FormatterInterface
335: {
336: return $this->maybeGetTtyTarget($level)->getFormatter();
337: }
338:
339: /**
340: * @param self::LEVEL_* $level
341: */
342: private function maybeGetTtyTarget(int $level): TargetStream
343: {
344: /** @var Target[] */
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: * @inheritDoc
363: */
364: public function getStdoutTarget(): TargetStream
365: {
366: return $this->State->StdoutTarget ??= StreamTarget::fromStream(\STDOUT);
367: }
368:
369: /**
370: * @inheritDoc
371: */
372: public function getStderrTarget(): TargetStream
373: {
374: return $this->State->StderrTarget ??= StreamTarget::fromStream(\STDERR);
375: }
376:
377: /**
378: * @inheritDoc
379: */
380: public function getErrorCount(): int
381: {
382: return $this->State->ErrorCount;
383: }
384:
385: /**
386: * @inheritDoc
387: */
388: public function getWarningCount(): int
389: {
390: return $this->State->WarningCount;
391: }
392:
393: /**
394: * @inheritDoc
395: */
396: public function escape(string $string): string
397: {
398: return Formatter::escapeTags($string);
399: }
400:
401: /**
402: * @inheritDoc
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: /** @var float */
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: // If output is identical for success and failure, print a
427: // success message
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @inheritDoc
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: * @param array<self::LEVEL_*,Target[]> $targets
833: * @return Target[]
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: * Send a message to registered targets
848: *
849: * @param self::LEVEL_* $level
850: * @param self::TYPE_* $type
851: * @return $this
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: * Send a message to registered targets once per run
866: *
867: * @param self::LEVEL_* $level
868: * @param self::TYPE_* $type
869: * @return $this
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: * Send a message to registered TTY targets
889: *
890: * @param self::LEVEL_* $level
891: * @param self::TYPE_* $type
892: * @return $this
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: * @template T of Target
907: *
908: * @param self::LEVEL_* $level
909: * @param self::TYPE_* $type
910: * @param array<self::LEVEL_*,T[]> $targets
911: * @return $this
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: // As per PSR-3 Section 1.3
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: * @param array<int,ConsoleTargetInterface> $targets
976: * @param int-mask-of<TargetTypeFlag::*> $flags
977: * @return array<int,ConsoleTargetInterface>
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: * @internal
1000: */
1001: final class ConsoleState
1002: {
1003: /** @var array<Console::LEVEL_*,TargetStream[]> */
1004: public array $StdioTargetsByLevel = [];
1005: /** @var array<Console::LEVEL_*,TargetStream[]> */
1006: public array $TtyTargetsByLevel = [];
1007: /** @var array<Console::LEVEL_*,Target[]> */
1008: public array $TargetsByLevel = [];
1009: /** @var array<int,Target> */
1010: public array $Targets = [];
1011: /** @var array<int,Target> */
1012: public array $DeregisteredTargets = [];
1013: /** @var array<int,int-mask-of<TargetTypeFlag::*>> */
1014: public array $TargetTypeFlags = [];
1015: public ?TargetStream $StdoutTarget = null;
1016: public ?TargetStream $StderrTarget = null;
1017: public int $GroupLevel = -1;
1018: /** @var array<array{string|null,string|null}> */
1019: public array $GroupMessageStack = [];
1020: public int $ErrorCount = 0;
1021: public int $WarningCount = 0;
1022: /** @var array<string,true> */
1023: public array $Written = [];
1024: /** @var string[] */
1025: public array $LastWritten = [];
1026: /** @var array{int<0,max>,float}|null */
1027: public ?array $SpinnerState;
1028: public LoggerInterface $Logger;
1029:
1030: private function __clone() {}
1031: }
1032: