Initial commit

This commit is contained in:
2025-08-04 16:33:07 +03:30
commit f798e8e35c
9595 changed files with 1208683 additions and 0 deletions

View File

@@ -0,0 +1,107 @@
<?php
namespace Laravel\Pail\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Process\Exceptions\ProcessTimedOutException;
use Laravel\Pail\File;
use Laravel\Pail\Guards\EnsurePcntlIsAvailable;
use Laravel\Pail\Options;
use Laravel\Pail\ProcessFactory;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Process\Exception\ProcessSignaledException;
use function Termwind\render;
use function Termwind\renderUsing;
#[AsCommand(name: 'pail')]
class PailCommand extends Command
{
/**
* {@inheritDoc}
*/
protected $signature = 'pail
{--filter= : Filter the logs by the given value}
{--message= : Filter the logs by the given message}
{--level= : Filter the logs by the given level}
{--auth= : Filter the logs by the given authenticated ID}
{--user= : Filter the logs by the given authenticated ID (alias for --auth)}
{--timeout=3600 : The maximum execution time in seconds}';
/**
* {@inheritDoc}
*/
protected $description = 'Tails the application logs.';
/**
* The file instance, if any.
*/
protected ?File $file = null;
/**
* Handles the command execution.
*/
public function handle(ProcessFactory $processFactory): void
{
EnsurePcntlIsAvailable::check();
renderUsing($this->output);
render(<<<'HTML'
<div class="max-w-150 mx-2 mt-1 flex">
<div>
<span class="px-1 bg-blue uppercase text-white">INFO</span>
<span class="flex-1">
<span class="ml-1 ">Tailing application logs.</span>
</span>
</div>
<span class="flex-1"></span>
<span class="text-gray ml-1">
<span class="text-gray">Press Ctrl+C to exit</span>
</span>
</div>
HTML,
);
render(<<<'HTML'
<div class="max-w-150 mx-2 flex">
<div>
</div>
<span class="flex-1"></span>
<span class="text-gray ml-1">
<span class="text-gray">Use -v|-vv to show more details</span>
</span>
</div>
HTML,
);
$this->file = new File(storage_path('pail/'.uniqid().'.pail'));
$this->file->create();
$this->trap([SIGINT, SIGTERM], fn () => $this->file->destroy());
$options = Options::fromCommand($this);
assert($this->file instanceof File);
try {
$processFactory->run($this->file, $this->output, $this->laravel->basePath(), $options);
} catch (ProcessSignaledException $e) {
if (in_array($e->getSignal(), [SIGINT, SIGTERM], true)) {
$this->newLine();
}
} catch (ProcessTimedOutException $e) {
$this->components->info('Maximum execution time exceeded.');
} finally {
$this->file?->destroy();
}
}
/**
* Handles the object destruction.
*/
public function __destruct()
{
if ($this->file) {
$this->file->destroy();
}
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Laravel\Pail\Contracts;
use Laravel\Pail\ValueObjects\MessageLogged;
interface Printer
{
/**
* Prints the given message logged.
*/
public function print(MessageLogged $messageLogged): void;
}

100
vendor/laravel/pail/src/File.php vendored Normal file
View File

@@ -0,0 +1,100 @@
<?php
namespace Laravel\Pail;
use Stringable;
class File implements Stringable
{
/**
* The time to live of the file.
*/
protected const TTL = 3600;
/**
* Creates a new instance of the file.
*/
public function __construct(
protected string $file,
) {
//
}
/**
* Ensure the file exists.
*/
public function create(): void
{
if (! $this->exists()) {
$directory = dirname($this->file);
if (! is_dir($directory)) {
mkdir($directory, 0755, true);
file_put_contents($directory.'/.gitignore', "*\n!.gitignore\n");
}
touch($this->file);
}
}
/**
* Determines if the file exists.
*/
public function exists(): bool
{
return file_exists($this->file);
}
/**
* Deletes the file.
*/
public function destroy(): void
{
if ($this->exists()) {
unlink($this->file);
}
}
/**
* Log a log message to the file.
*
* @param array<string, mixed> $context
*/
public function log(string $level, string $message, array $context = []): void
{
if ($this->isStale()) {
$this->destroy();
return;
}
$loggerFactory = new LoggerFactory($this);
$logger = $loggerFactory->create();
$logger->log($level, $message, $context);
}
/**
* Returns the file as string.
*/
public function __toString(): string
{
return $this->file;
}
/**
* Determines if the file is staled.
*/
protected function isStale(): bool
{
$modificationTime = @filemtime($this->file);
if ($modificationTime === false) {
return true;
}
return time() - $modificationTime > static::TTL;
}
}

30
vendor/laravel/pail/src/Files.php vendored Normal file
View File

@@ -0,0 +1,30 @@
<?php
namespace Laravel\Pail;
use Illuminate\Support\Collection;
class Files
{
/**
* Creates a new instance of the files.
*/
public function __construct(
protected string $path,
) {
//
}
/**
* Returns the list of files.
*
* @return \Illuminate\Support\Collection<int, File>
*/
public function all(): Collection
{
$files = glob($this->path.'/*.pail') ?: [];
return collect($files)
->map(fn (string $file) => new File($file));
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Laravel\Pail\Guards;
use RuntimeException;
class EnsurePcntlIsAvailable
{
/**
* Checks if the pcntl extension is available.
*/
public static function check(): void
{
if (! function_exists('pcntl_fork')) {
throw new RuntimeException('The [pcntl] extension is required to run Pail.');
}
}
}

121
vendor/laravel/pail/src/Handler.php vendored Normal file
View File

@@ -0,0 +1,121 @@
<?php
namespace Laravel\Pail;
use Illuminate\Console\Events\CommandStarting;
use Illuminate\Contracts\Container\Container;
use Illuminate\Foundation\Auth\User;
use Illuminate\Log\Context\Repository as ContextRepository;
use Illuminate\Log\Events\MessageLogged;
use Illuminate\Queue\Events\JobExceptionOccurred;
use Illuminate\Queue\Events\JobProcessing;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Throwable;
class Handler
{
/**
* The last lifecycle captured event.
*/
protected CommandStarting|JobProcessing|JobExceptionOccurred|null $lastLifecycleEvent = null;
/**
* The artisan command being executed, if any.
*/
protected ?string $artisanCommand = null;
/**
* Creates a new instance of the handler.
*/
public function __construct(
protected Container $container,
protected Files $files,
protected bool $runningInConsole,
) {
//
}
/**
* Reports the given message logged.
*/
public function log(MessageLogged $messageLogged): void
{
$files = $this->files->all();
if ($files->isEmpty()) {
return;
}
$context = $this->context($messageLogged);
$files->each(
fn (File $file) => $file->log(
$messageLogged->level,
$messageLogged->message,
$context,
),
);
}
/**
* Sets the last application lifecycle event.
*/
public function setLastLifecycleEvent(CommandStarting|JobProcessing|JobExceptionOccurred|null $event): void
{
if ($event instanceof CommandStarting) {
$this->artisanCommand = $event->command;
}
$this->lastLifecycleEvent = $event;
}
/**
* Builds the context array.
*
* @return array<string, mixed>
*/
protected function context(MessageLogged $messageLogged): array
{
$context = ['__pail' => ['origin' => match (true) {
$this->artisanCommand && $this->lastLifecycleEvent && in_array($this->lastLifecycleEvent::class, [JobProcessing::class, JobExceptionOccurred::class]) => [
'type' => 'queue',
'command' => $this->artisanCommand,
'queue' => $this->lastLifecycleEvent->job->getQueue(),
'job' => $this->lastLifecycleEvent->job->resolveName(),
],
$this->runningInConsole => [
'type' => 'console',
'command' => $this->artisanCommand,
],
default => [
'type' => 'http',
'method' => request()->method(),
'path' => request()->path(),
'auth_id' => Auth::id(),
'auth_email' => Auth::user() instanceof User ? Auth::user()->email : null, // @phpstan-ignore property.notFound
],
}]];
if (isset($messageLogged->context['exception']) && $this->lastLifecycleEvent instanceof JobExceptionOccurred) {
if ($messageLogged->context['exception'] === $this->lastLifecycleEvent->exception) {
$this->setLastLifecycleEvent(null);
}
}
$context['__pail']['origin']['trace'] = isset($messageLogged->context['exception'])
&& $messageLogged->context['exception'] instanceof Throwable ? collect($messageLogged->context['exception']->getTrace())
->filter(fn (array $frame) => isset($frame['file']))
->map(fn (array $frame) => [
'file' => $frame['file'], // @phpstan-ignore offsetAccess.notFound
'line' => $frame['line'] ?? null,
])->values()
: null;
return collect($messageLogged->context)
->merge($context)
->when($this->container->bound(ContextRepository::class), function (Collection $context) {
return $context->merge($this->container->make(ContextRepository::class)->all()); // @phpstan-ignore method.nonObject
})->toArray();
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Laravel\Pail;
use Monolog\Formatter\JsonFormatter;
use Monolog\Handler\StreamHandler;
use Monolog\Level;
use Monolog\Logger;
use Psr\Log\LoggerInterface;
class LoggerFactory
{
/**
* Creates a new instance of the logger factory.
*/
public function __construct(
protected File $file,
) {
//
}
/**
* Creates a new instance of the logger.
*/
public function create(): LoggerInterface
{
$handler = new StreamHandler($this->file->__toString(), Level::Debug);
$handler->setFormatter(new JsonFormatter);
return new Logger('pail', [$handler]);
}
}

76
vendor/laravel/pail/src/Options.php vendored Normal file
View File

@@ -0,0 +1,76 @@
<?php
namespace Laravel\Pail;
use Illuminate\Console\Command;
use Laravel\Pail\ValueObjects\MessageLogged;
class Options
{
/**
* Creates a new instance of the tail options.
*/
public function __construct(
protected int $timeout,
protected ?string $authId,
protected ?string $level,
protected ?string $filter,
protected ?string $message,
) {
//
}
/**
* Creates a new instance of the tail options from the given console command.
*/
public static function fromCommand(Command $command): static
{
$authId = $command->option('auth') ?? $command->option('user');
assert(is_string($authId) || $authId === null);
$level = $command->option('level');
assert(is_string($level) || $level === null);
$filter = $command->option('filter');
assert(is_string($filter) || $filter === null);
$message = $command->option('message');
assert(is_string($message) || $message === null);
$timeout = (int) $command->option('timeout');
return new static($timeout, $authId, $level, $filter, $message);
}
/**
* Whether the tail options accept the given message logged.
*/
public function accepts(MessageLogged $messageLogged): bool
{
if (is_string($this->authId) && $messageLogged->authId() !== $this->authId) {
return false;
}
if (is_string($this->level) && strtolower($messageLogged->level()) !== strtolower($this->level)) {
return false;
}
if (is_string($this->filter) && ! str_contains(strtolower((string) $messageLogged), strtolower($this->filter))) {
return false;
}
if (is_string($this->message) && ! str_contains(strtolower($messageLogged->message()), strtolower($this->message))) {
return false;
}
return true;
}
/**
* Returns the number of seconds before the process is killed.
*/
public function timeout(): int
{
return $this->timeout;
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Laravel\Pail;
use Illuminate\Console\Events\CommandStarting;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Log\Events\MessageLogged;
use Illuminate\Queue\Events\JobExceptionOccurred;
use Illuminate\Queue\Events\JobProcessed;
use Illuminate\Queue\Events\JobProcessing;
use Illuminate\Support\ServiceProvider;
use Laravel\Pail\Console\Commands\PailCommand;
class PailServiceProvider extends ServiceProvider
{
/**
* Registers the application services.
*/
public function register(): void
{
$this->app->singleton(
Files::class,
fn (Application $app) => new Files($app->storagePath('pail'))
);
$this->app->singleton(Handler::class, fn (Application $app) => new Handler(
$app,
$app->make(Files::class), // @phpstan-ignore argument.type
$app->runningInConsole(),
));
}
/**
* Bootstraps the application services.
*/
public function boot(): void
{
if (! $this->runningPailTests() && ($this->app->runningUnitTests() || ($_ENV['VAPOR_SSM_PATH'] ?? false))) {
return;
}
/** @var \Illuminate\Contracts\Events\Dispatcher $events */
$events = $this->app->make('events');
$events->listen(MessageLogged::class, function (MessageLogged $messageLogged) {
/** @var Handler $handler */
$handler = $this->app->make(Handler::class);
$handler->log($messageLogged);
});
$events->listen([CommandStarting::class, JobProcessing::class, JobExceptionOccurred::class], function (CommandStarting|JobProcessing|JobExceptionOccurred $lifecycleEvent) {
/** @var Handler $handler */
$handler = $this->app->make(Handler::class);
$handler->setLastLifecycleEvent($lifecycleEvent);
});
$events->listen([JobProcessed::class], function () {
/** @var Handler $handler */
$handler = $this->app->make(Handler::class);
$handler->setLastLifecycleEvent(null);
});
if ($this->app->runningInConsole()) {
$this->commands([
PailCommand::class,
]);
}
}
/**
* Determines if the Pail's test suite is running.
*/
protected function runningPailTests(): bool
{
return $_ENV['PAIL_TESTS'] ?? false;
}
}

View File

@@ -0,0 +1,278 @@
<?php
namespace Laravel\Pail\Printers;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Laravel\Pail\Contracts\Printer;
use Laravel\Pail\ValueObjects\MessageLogged;
use Laravel\Pail\ValueObjects\Origin\Http;
use Laravel\Pail\ValueObjects\Origin\Queue;
use Symfony\Component\Console\Output\OutputInterface;
use function Termwind\render;
use function Termwind\renderUsing;
use function Termwind\terminal;
class CliPrinter implements Printer
{
/**
* {@inheritDoc}
*/
public function print(MessageLogged $messageLogged): void
{
$classOrType = $this->truncateClassOrType($messageLogged->classOrType());
$color = $messageLogged->color();
$message = $this->truncateMessage($messageLogged->message());
$date = $this->output->isVerbose() ? $messageLogged->date() : $messageLogged->time();
$fileHtml = $this->fileHtml($messageLogged->file(), $classOrType);
$messageHtml = $this->messageHtml($message);
$optionsHtml = $this->optionsHtml($messageLogged);
$traceHtml = $this->traceHtml($messageLogged);
$messageClasses = $this->output->isVerbose() ? '' : 'truncate';
$endingTopRight = $this->output->isVerbose() ? '' : '┐';
$endingMiddle = $this->output->isVerbose() ? '' : '│';
$endingBottomRight = $this->output->isVerbose() ? '' : '┘';
renderUsing($this->output);
render(<<<HTML
<div class="max-w-150">
<div class="flex">
<div>
<span class="mr-1 text-gray">┌</span>
<span class="text-gray">$date</span>
<span class="px-1 text-$color font-bold">$classOrType</span>
</div>
<span class="flex-1 content-repeat-[─] text-gray"></span>
<span class="text-gray">
$fileHtml
<span class="text-gray">$endingTopRight</span>
</span>
</div>
<div class="flex $messageClasses">
<span>
<span class="mr-1 text-gray">│</span>
$messageHtml
</span>
<span class="flex-1"></span>
<span class="flex-1 text-gray text-right">$endingMiddle</span>
</div>
$traceHtml
<div class="flex text-gray">
<span>└</span>
<span class="mr-1 flex-1 content-repeat-[─]"></span>
$optionsHtml
<span class="ml-1">$endingBottomRight</span>
</div>
</div>
HTML);
}
/**
* Creates a new instance printer instance.
*/
public function __construct(protected OutputInterface $output, protected string $basePath)
{
//
}
/**
* Gets the file html.
*/
protected function fileHtml(?string $file, string $classOrType): ?string
{
if (is_null($file)) {
return null;
}
if ($_ENV['PAIL_TESTS'] ?? false) {
$file = $this->basePath.'/app/MyClass.php:12';
}
$file = str_replace($this->basePath.'/', '', $file);
if (! $this->output->isVerbose()) {
$file = Str::of($file)
->explode('/')
->when(
fn (Collection $file) => $file->count() > 4,
fn (Collection $file) => $file->take(2)->merge(
['…', (string) $file->last()],
),
)->implode('/');
$fileSize = max(0, min(terminal()->width() - strlen($classOrType) - 16, 145));
if (strlen($file) > $fileSize) {
$file = mb_substr($file, 0, $fileSize).'…';
}
}
if ($file === '…') {
return null;
}
$file = str_replace('……', '…', $file);
return <<<HTML
<span class="text-gray mx-1">
$file
</span>
HTML;
}
/**
* Gets the message html.
*/
protected function messageHtml(string $message): string
{
if (empty($message)) {
return '<span class="text-gray">No message.</span>';
}
$message = htmlspecialchars($message);
return "<span>$message</span>";
}
/**
* Truncates the class or type, if needed.
*/
protected function truncateClassOrType(string $classOrType): string
{
if ($this->output->isVerbose()) {
return $classOrType;
}
return Str::of($classOrType)
->explode('\\')
->when(
fn (Collection $classOrType) => $classOrType->count() > 4,
fn (Collection $classOrType) => $classOrType->take(2)->merge(
['…', (string) $classOrType->last()]
),
)->implode('\\');
}
/**
* Truncates the message, if needed.
*/
protected function truncateMessage(string $message): string
{
if (! $this->output->isVerbose()) {
$messageSize = max(0, min(terminal()->width() - 5, 145));
if (strlen($message) > $messageSize) {
$message = mb_substr($message, 0, $messageSize).'…';
}
}
return $message;
}
/**
* Gets the options html.
*/
public function optionsHtml(MessageLogged $messageLogged): string
{
$origin = $messageLogged->origin();
if ($origin instanceof Http) {
if (str_starts_with($path = $origin->path, '/') === false) {
$path = '/'.$origin->path;
}
$options = [
strtoupper($origin->method) => $path,
'Auth ID' => $origin->authId
? ($origin->authId.($origin->authEmail ? " ({$origin->authEmail})" : ''))
: 'guest',
];
} elseif ($origin instanceof Queue) {
$options = [
$origin->command ? "artisan {$origin->command}" : null,
$origin->queue,
$origin->job,
];
} else {
$options = [
$origin->command ? "artisan {$origin->command}" : 'artisan',
];
}
return collect($options)->merge(
$messageLogged->context() // @phpstan-ignore argument.type
)->reject(fn (mixed $value, string|int $key) => is_int($key) && is_null($value))
->map(fn (mixed $value) => is_string($value) ? $value : var_export($value, true))
->map(fn (string $value) => htmlspecialchars($value))
->map(fn (string $value, string|int $key) => is_string($key) ? "$key: $value" : $value)
->map(fn (string $value) => "<span class=\"font-bold\">$value</span>")
->implode(' • ');
}
/**
* Gets the trace html.
*/
public function traceHtml(MessageLogged $messageLogged): string
{
if (! $this->output->isVeryVerbose()) {
return '';
}
$trace = $messageLogged->trace();
if ($_ENV['PAIL_TESTS'] ?? false) {
$trace = [
[
'line' => 12,
'file' => $this->basePath.'/app/MyClass.php',
],
[
'line' => 34,
'file' => $this->basePath.'/app/MyClass.php',
],
];
}
if (is_null($trace)) {
return '';
}
return collect($trace)
->map(function (array $frame, int $index) {
$number = $index + 1;
[
'line' => $line,
'file' => $file,
] = $frame;
$file = str_replace($this->basePath.'/', '', $file);
$remainingTraces = '';
if (! $this->output->isVerbose()) {
$file = (string) Str::of($file)
->explode('/')
->when(
fn (Collection $file) => $file->count() > 4,
fn (Collection $file) => $file->take(2)->merge(
['…', (string) $file->last()],
),
)->implode('/');
}
return <<<HTML
<div class="flex text-gray">
<span>
<span class="mr-1 text-gray">│</span>
<span>$number. $file:$line $remainingTraces</span>
</span>
</div>
HTML;
})->implode('');
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Laravel\Pail;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Str;
use Laravel\Pail\Printers\CliPrinter;
use Laravel\Pail\ValueObjects\MessageLogged;
use Symfony\Component\Console\Output\OutputInterface;
class ProcessFactory
{
/**
* Creates a new instance of the process factory.
*/
public function run(File $file, OutputInterface $output, string $basePath, Options $options): void
{
$printer = new CliPrinter($output, $basePath);
$remainingBuffer = '';
Process::timeout($options->timeout())
->tty(false)
->run(
$this->command($file),
function (string $type, string $buffer) use ($options, $printer, &$remainingBuffer) {
$lines = Str::of($buffer)->explode("\n");
if ($remainingBuffer !== '' && isset($lines[0])) {
$lines[0] = $remainingBuffer.$lines[0];
$remainingBuffer = '';
}
if ($lines->last() === '') {
$lines = $lines->slice(0, -1);
} elseif (! str_ends_with((string) $lines->last(), "\n")) {
$remainingBuffer = $lines->pop();
}
$lines
->filter(fn (string $line) => $line !== '')
->map(fn (string $line) => MessageLogged::fromJson($line))
->filter(fn (MessageLogged $messageLogged) => $options->accepts($messageLogged))
->each(fn (MessageLogged $messageLogged) => $printer->print($messageLogged));
}
);
}
/**
* Returns the raw command.
*/
protected function command(File $file): string
{
return '\\tail -F "'.$file->__toString().'"';
}
}

View File

@@ -0,0 +1,180 @@
<?php
namespace Laravel\Pail\ValueObjects;
use Illuminate\Support\Carbon;
use Stringable;
class MessageLogged implements Stringable
{
/**
* Creates a new instance of the message logged.
*
* @param array{__pail: array{origin: array{trace: array<int, array{file: string, line: int}>|null, type: string, queue: string, job: string, command: string, method: string, path: string, auth_id: ?string, auth_email: ?string}}, exception: array{class: string, file: string}} $context
*/
protected function __construct(
protected string $message,
protected string $datetime,
protected string $levelName,
protected array $context,
) {
//
}
/**
* Creates a new instance of the message logged from a json string.
*/
public static function fromJson(string $json): static
{
/** @var array{message: string, context: array{__pail: array{origin: array{trace: array<int, array{file: string, line: int}>|null, type: string, queue: string, job: string, command: string, method: string, path: string, auth_id: ?string, auth_email: ?string}}, exception: array{class: string, file: string}}, level_name: string, datetime: string} $array */
$array = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
[
'message' => $message,
'datetime' => $datetime,
'level_name' => $levelName,
'context' => $context,
] = $array;
return new static($message, $datetime, $levelName, $context);
}
/**
* Gets the log message's message.
*/
public function message(): string
{
return $this->message;
}
/**
* Gets the log message's date.
*/
public function date(): string
{
if ($_ENV['PAIL_TESTS'] ?? false) {
return '2024-01-01 03:04:05';
}
$time = Carbon::createFromFormat('Y-m-d\TH:i:s.uP', $this->datetime);
assert($time instanceof Carbon);
return $time->format('Y-m-d H:i:s');
}
/**
* Gets the log message's time.
*/
public function time(): string
{
if ($_ENV['PAIL_TESTS'] ?? false) {
return '03:04:05';
}
$time = Carbon::createFromFormat('Y-m-d\TH:i:s.uP', $this->datetime);
assert($time instanceof Carbon);
return $time->format('H:i:s');
}
/**
* Gets the log message's class.
*/
public function classOrType(): string
{
return $this->context['exception']['class'] ?? strtoupper($this->levelName);
}
/**
* Gets the log message's color.
*/
public function color(): string
{
return match ($this->levelName) {
'DEBUG' => 'gray',
'INFO' => 'blue',
'NOTICE' => 'yellow',
'WARNING' => 'yellow',
'ERROR' => 'red',
'CRITICAL' => 'red',
'ALERT' => 'red',
'EMERGENCY' => 'red',
default => 'gray',
};
}
/**
* Gets the log message's level.
*/
public function level(): string
{
return $this->levelName;
}
/**
* Gets the log message's file, if any.
*/
public function file(): ?string
{
return $this->context['exception']['file'] ?? null;
}
/**
* Gets the log message's auth id.
*/
public function authId(): ?string
{
return $this->context['__pail']['origin']['auth_id'] ?? null;
}
/**
* Gets the log message's origin.
*/
public function origin(): Origin\Console|Origin\Http|Origin\Queue
{
return match ($this->context['__pail']['origin']['type']) {
'console' => Origin\Console::fromArray($this->context['__pail']['origin']),
'queue' => Origin\Queue::fromArray($this->context['__pail']['origin']),
default => Origin\Http::fromArray($this->context['__pail']['origin']),
};
}
/**
* Gets the log message's trace, if any.
*
* @return array<int, array{file: string, line: int}>|null
*/
public function trace(): ?array
{
return $this->context['__pail']['origin']['trace'] ?? null;
}
/**
* Gets the log message's context.
*
* @return array<string, mixed>
*/
public function context(): array
{
return collect($this->context)->except([
'__pail',
'exception',
'userId',
])->toArray();
}
/**
* {@inheritDoc}
*/
public function __toString(): string
{
return json_encode([
'message' => $this->message,
'datetime' => $this->datetime,
'level_name' => $this->levelName,
'context' => $this->context,
], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Laravel\Pail\ValueObjects\Origin;
class Console
{
/**
* Creates a new instance of the console origin.
*/
public function __construct(
public ?string $command,
) {
//
}
/**
* Creates a new instance of the console origin from the given json string.
*
* @param array{command?: string} $array
*/
public static function fromArray(array $array): static
{
$command = $array['command'] ?? null;
return new static($command);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Laravel\Pail\ValueObjects\Origin;
class Http
{
/**
* Creates a new instance of the http origin.
*/
public function __construct(
public string $method,
public string $path,
public ?string $authId,
public ?string $authEmail,
) {
//
}
/**
* Creates a new instance of the http origin from the given json string.
*
* @param array{method: string, path: string, auth_id: ?string, auth_email: ?string} $array
*/
public static function fromArray(array $array): static
{
['method' => $method, 'path' => $path, 'auth_id' => $authId, 'auth_email' => $authEmail] = $array;
return new static($method, $path, $authId, $authEmail);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Laravel\Pail\ValueObjects\Origin;
class Queue
{
/**
* Creates a new instance of the console origin.
*/
public function __construct(
public string $queue,
public string $job,
public ?string $command,
) {
//
}
/**
* Creates a new instance of the queue origin from the given json string.
*
* @param array{queue: string, job: string, command: ?string} $array
*/
public static function fromArray(array $array): static
{
[
'queue' => $queue,
'job' => $job,
'command' => $command,
] = $array;
return new static($queue, $job, $command);
}
}