vendor and env first commit

This commit is contained in:
2025-03-28 08:52:46 +01:00
parent f8388bc81b
commit 8f26283832
10976 changed files with 1349952 additions and 2 deletions
@@ -0,0 +1,33 @@
<?php
namespace Spatie\LaravelIgnition\Recorders\DumpRecorder;
class Dump
{
protected string $htmlDump;
protected ?string $file;
protected ?int $lineNumber;
protected float $microtime;
public function __construct(string $htmlDump, ?string $file, ?int $lineNumber, ?float $microtime = null)
{
$this->htmlDump = $htmlDump;
$this->file = $file;
$this->lineNumber = $lineNumber;
$this->microtime = $microtime ?? microtime(true);
}
/** @return array<string, mixed> */
public function toArray(): array
{
return [
'html_dump' => $this->htmlDump,
'file' => $this->file,
'line_number' => $this->lineNumber,
'microtime' => $this->microtime,
];
}
}
@@ -0,0 +1,22 @@
<?php
namespace Spatie\LaravelIgnition\Recorders\DumpRecorder;
use Symfony\Component\VarDumper\Cloner\VarCloner;
class DumpHandler
{
protected DumpRecorder $dumpRecorder;
public function __construct(DumpRecorder $dumpRecorder)
{
$this->dumpRecorder = $dumpRecorder;
}
public function dump(mixed $value): void
{
$data = (new VarCloner)->cloneVar($value);
$this->dumpRecorder->record($data);
}
}
@@ -0,0 +1,135 @@
<?php
namespace Spatie\LaravelIgnition\Recorders\DumpRecorder;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Arr;
use ReflectionMethod;
use ReflectionProperty;
use Symfony\Component\VarDumper\Cloner\Data;
use Symfony\Component\VarDumper\VarDumper;
class DumpRecorder
{
/** @var array<array<int,mixed>> */
protected array $dumps = [];
protected Application $app;
protected static bool $registeredHandler = false;
public function __construct(Application $app)
{
$this->app = $app;
}
public function start(): self
{
$multiDumpHandler = new MultiDumpHandler();
$this->app->singleton(MultiDumpHandler::class, fn () => $multiDumpHandler);
if (! self::$registeredHandler) {
static::$registeredHandler = true;
$this->ensureOriginalHandlerExists();
$originalHandler = VarDumper::setHandler(fn ($dumpedVariable) => $multiDumpHandler->dump($dumpedVariable));
$multiDumpHandler?->addHandler($originalHandler);
$multiDumpHandler->addHandler(fn ($var) => (new DumpHandler($this))->dump($var));
}
return $this;
}
public function record(Data $data): void
{
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 11);
$sourceFrame = $this->findSourceFrame($backtrace);
$file = (string) Arr::get($sourceFrame, 'file');
$lineNumber = (int) Arr::get($sourceFrame, 'line');
$htmlDump = (new HtmlDumper())->dump($data);
$this->dumps[] = new Dump($htmlDump, $file, $lineNumber);
}
public function getDumps(): array
{
return $this->toArray();
}
public function reset()
{
$this->dumps = [];
}
public function toArray(): array
{
$dumps = [];
foreach ($this->dumps as $dump) {
$dumps[] = $dump->toArray();
}
return $dumps;
}
/*
* Only the `VarDumper` knows how to create the orignal HTML or CLI VarDumper.
* Using reflection and the private VarDumper::register() method we can force it
* to create and register a new VarDumper::$handler before we'll overwrite it.
* Of course, we only need to do this if there isn't a registered VarDumper::$handler.
*
* @throws \ReflectionException
*/
protected function ensureOriginalHandlerExists(): void
{
$reflectionProperty = new ReflectionProperty(VarDumper::class, 'handler');
$reflectionProperty->setAccessible(true);
$handler = $reflectionProperty->getValue();
if (! $handler) {
// No handler registered yet, so we'll force VarDumper to create one.
$reflectionMethod = new ReflectionMethod(VarDumper::class, 'register');
$reflectionMethod->setAccessible(true);
$reflectionMethod->invoke(null);
}
}
/**
* Find the first meaningful stack frame that is not the `DumpRecorder` itself.
*
* @template T of array{class?: class-string, function?: string, line?: int, file?: string}
*
* @param array<T> $stacktrace
*
* @return null|T
*/
protected function findSourceFrame(array $stacktrace): ?array
{
$seenVarDumper = false;
foreach ($stacktrace as $frame) {
// Keep looping until we're past the VarDumper::dump() call in Symfony's helper functions file.
if (Arr::get($frame, 'class') === VarDumper::class && Arr::get($frame, 'function') === 'dump') {
$seenVarDumper = true;
continue;
}
if (! $seenVarDumper) {
continue;
}
// Return the next frame in the stack after the VarDumper::dump() call:
return $frame;
}
return null;
}
}
@@ -0,0 +1,34 @@
<?php
namespace Spatie\LaravelIgnition\Recorders\DumpRecorder;
use Symfony\Component\VarDumper\Cloner\Data;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\HtmlDumper as BaseHtmlDumper;
class HtmlDumper extends BaseHtmlDumper
{
public function __construct($output = null, string $charset = null, int $flags = 0)
{
parent::__construct($output, $charset, $flags);
$this->setDumpHeader('');
}
public function dumpVariable($variable): string
{
$cloner = new VarCloner();
$clonedData = $cloner->cloneVar($variable)->withMaxDepth(3);
return $this->dump($clonedData);
}
public function dump(Data $data, $output = null, array $extraDisplayOptions = []): string
{
return (string)parent::dump($data, true, [
'maxDepth' => 3,
'maxStringLength' => 160,
]);
}
}
@@ -0,0 +1,25 @@
<?php
namespace Spatie\LaravelIgnition\Recorders\DumpRecorder;
class MultiDumpHandler
{
/** @var array<int, callable|null> */
protected array $handlers = [];
public function dump(mixed $value): void
{
foreach ($this->handlers as $handler) {
if ($handler) {
$handler($value);
}
}
}
public function addHandler(callable $callable = null): self
{
$this->handlers[] = $callable;
return $this;
}
}
@@ -0,0 +1,177 @@
<?php
namespace Spatie\LaravelIgnition\Recorders\JobRecorder;
use DateTime;
use Error;
use Exception;
use Illuminate\Contracts\Encryption\Encrypter;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\Queue\Job;
use Illuminate\Queue\CallQueuedClosure;
use Illuminate\Queue\Events\JobExceptionOccurred;
use Illuminate\Queue\Jobs\RedisJob;
use Illuminate\Support\Str;
use ReflectionClass;
use ReflectionProperty;
use RuntimeException;
class JobRecorder
{
protected ?Job $job = null;
public function __construct(
protected Application $app,
protected int $maxChainedJobReportingDepth = 5,
) {
}
public function start(): self
{
/** @phpstan-ignore-next-line */
$this->app['events']->listen(JobExceptionOccurred::class, [$this, 'record']);
return $this;
}
public function record(JobExceptionOccurred $event): void
{
$this->job = $event->job;
}
/**
* @return array<string, mixed>|null
*/
public function getJob(): ?array
{
if ($this->job === null) {
return null;
}
return array_merge(
$this->getJobProperties(),
[
'name' => $this->job->resolveName(),
'connection' => $this->job->getConnectionName(),
'queue' => $this->job->getQueue(),
]
);
}
public function reset(): void
{
$this->job = null;
}
protected function getJobProperties(): array
{
$payload = collect($this->resolveJobPayload());
$properties = [];
foreach ($payload as $key => $value) {
if (! in_array($key, ['job', 'data', 'displayName'])) {
$properties[$key] = $value;
}
}
try {
if (is_string($payload['data'])) {
$properties['data'] = json_decode($payload['data'], true, 512, JSON_THROW_ON_ERROR);
}
} catch (Exception $exception) {
}
if ($pushedAt = DateTime::createFromFormat('U.u', $payload->get('pushedAt', ''))) {
$properties['pushedAt'] = $pushedAt->format(DATE_ATOM);
}
try {
$properties['data'] = $this->resolveCommandProperties(
$this->resolveObjectFromCommand($payload['data']['command']),
$this->maxChainedJobReportingDepth
);
} catch (Exception $exception) {
}
return $properties;
}
protected function resolveJobPayload(): array
{
if (! $this->job instanceof RedisJob) {
return $this->job->payload();
}
try {
return json_decode($this->job->getReservedJob(), true, 512, JSON_THROW_ON_ERROR);
} catch (Exception $e) {
return $this->job->payload();
}
}
protected function resolveCommandProperties(object $command, int $maxChainDepth): array
{
$propertiesToIgnore = ['job', 'closure'];
$properties = collect((new ReflectionClass($command))->getProperties())
->reject(function (ReflectionProperty $property) use ($propertiesToIgnore) {
return in_array($property->name, $propertiesToIgnore);
})
->mapWithKeys(function (ReflectionProperty $property) use ($command) {
try {
$property->setAccessible(true);
return [$property->name => $property->getValue($command)];
} catch (Error $error) {
return [$property->name => 'uninitialized'];
}
});
if ($properties->has('chained')) {
$properties['chained'] = $this->resolveJobChain($properties->get('chained'), $maxChainDepth);
}
return $properties->all();
}
/**
* @param array<string, mixed> $chainedCommands
* @param int $maxDepth
*
* @return array
*/
protected function resolveJobChain(array $chainedCommands, int $maxDepth): array
{
if ($maxDepth === 0) {
return ['Ignition stopped recording jobs after this point since the max chain depth was reached'];
}
return array_map(
function (string $command) use ($maxDepth) {
$commandObject = $this->resolveObjectFromCommand($command);
return [
'name' => $commandObject instanceof CallQueuedClosure ? $commandObject->displayName() : get_class($commandObject),
'data' => $this->resolveCommandProperties($commandObject, $maxDepth - 1),
];
},
$chainedCommands
);
}
// Taken from Illuminate\Queue\CallQueuedHandler
protected function resolveObjectFromCommand(string $command): object
{
if (Str::startsWith($command, 'O:')) {
return unserialize($command);
}
if ($this->app->bound(Encrypter::class)) {
/** @phpstan-ignore-next-line */
return unserialize($this->app[Encrypter::class]->decrypt($command));
}
throw new RuntimeException('Unable to extract job payload.');
}
}
@@ -0,0 +1,55 @@
<?php
namespace Spatie\LaravelIgnition\Recorders\LogRecorder;
use Illuminate\Log\Events\MessageLogged;
class LogMessage
{
protected ?string $message;
protected string $level;
/** @var array<string, string> */
protected array $context = [];
protected ?float $microtime;
/**
* @param string|null $message
* @param string $level
* @param array<string, string> $context
* @param float|null $microtime
*/
public function __construct(
?string $message,
string $level,
array $context = [],
?float $microtime = null
) {
$this->message = $message;
$this->level = $level;
$this->context = $context;
$this->microtime = $microtime ?? microtime(true);
}
public static function fromMessageLoggedEvent(MessageLogged $event): self
{
return new self(
$event->message,
$event->level,
$event->context
);
}
/** @return array<string, mixed> */
public function toArray(): array
{
return [
'message' => $this->message,
'level' => $this->level,
'context' => $this->context,
'microtime' => $this->microtime,
];
}
}
@@ -0,0 +1,93 @@
<?php
namespace Spatie\LaravelIgnition\Recorders\LogRecorder;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Log\Events\MessageLogged;
use Throwable;
class LogRecorder
{
/** @var \Spatie\LaravelIgnition\Recorders\LogRecorder\LogMessage[] */
protected array $logMessages = [];
protected Application $app;
protected ?int $maxLogs;
public function __construct(Application $app, ?int $maxLogs = null)
{
$this->app = $app;
$this->maxLogs = $maxLogs;
}
public function start(): self
{
/** @phpstan-ignore-next-line */
$this->app['events']->listen(MessageLogged::class, [$this, 'record']);
return $this;
}
public function record(MessageLogged $event): void
{
if ($this->shouldIgnore($event)) {
return;
}
$this->logMessages[] = LogMessage::fromMessageLoggedEvent($event);
if (is_int($this->maxLogs)) {
$this->logMessages = array_slice($this->logMessages, -$this->maxLogs);
}
}
/** @return array<array<int,string>> */
public function getLogMessages(): array
{
return $this->toArray();
}
/** @return array<int, mixed> */
public function toArray(): array
{
$logMessages = [];
foreach ($this->logMessages as $log) {
$logMessages[] = $log->toArray();
}
return $logMessages;
}
protected function shouldIgnore(mixed $event): bool
{
if (! isset($event->context['exception'])) {
return false;
}
if (! $event->context['exception'] instanceof Throwable) {
return false;
}
return true;
}
public function reset(): void
{
$this->logMessages = [];
}
public function getMaxLogs(): ?int
{
return $this->maxLogs;
}
public function setMaxLogs(?int $maxLogs): self
{
$this->maxLogs = $maxLogs;
return $this;
}
}
@@ -0,0 +1,65 @@
<?php
namespace Spatie\LaravelIgnition\Recorders\QueryRecorder;
use Illuminate\Database\Events\QueryExecuted;
class Query
{
protected string $sql;
protected float $time;
protected string $connectionName;
/** @var array<string, string>|null */
protected ?array $bindings;
protected float $microtime;
public static function fromQueryExecutedEvent(QueryExecuted $queryExecuted, bool $reportBindings = false): self
{
return new self(
$queryExecuted->sql,
$queryExecuted->time,
/** @phpstan-ignore-next-line */
$queryExecuted->connectionName ?? '',
$reportBindings ? $queryExecuted->bindings : null
);
}
/**
* @param string $sql
* @param float $time
* @param string $connectionName
* @param array<string, string>|null $bindings
* @param float|null $microtime
*/
protected function __construct(
string $sql,
float $time,
string $connectionName,
?array $bindings = null,
?float $microtime = null
) {
$this->sql = $sql;
$this->time = $time;
$this->connectionName = $connectionName;
$this->bindings = $bindings;
$this->microtime = $microtime ?? microtime(true);
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'sql' => $this->sql,
'time' => $this->time,
'connection_name' => $this->connectionName,
'bindings' => $this->bindings,
'microtime' => $this->microtime,
];
}
}
@@ -0,0 +1,88 @@
<?php
namespace Spatie\LaravelIgnition\Recorders\QueryRecorder;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Database\Events\QueryExecuted;
class QueryRecorder
{
/** @var \Spatie\LaravelIgnition\Recorders\QueryRecorder\Query[] */
protected array $queries = [];
protected Application $app;
protected bool $reportBindings = true;
protected ?int $maxQueries;
public function __construct(
Application $app,
bool $reportBindings = true,
?int $maxQueries = 200
) {
$this->app = $app;
$this->reportBindings = $reportBindings;
$this->maxQueries = $maxQueries;
}
public function start(): self
{
/** @phpstan-ignore-next-line */
$this->app['events']->listen(QueryExecuted::class, [$this, 'record']);
return $this;
}
public function record(QueryExecuted $queryExecuted): void
{
$this->queries[] = Query::fromQueryExecutedEvent($queryExecuted, $this->reportBindings);
if (is_int($this->maxQueries)) {
$this->queries = array_slice($this->queries, -$this->maxQueries);
}
}
/**
* @return array<int, array<string, mixed>>
*/
public function getQueries(): array
{
$queries = [];
foreach ($this->queries as $query) {
$queries[] = $query->toArray();
}
return $queries;
}
public function reset(): void
{
$this->queries = [];
}
public function getReportBindings(): bool
{
return $this->reportBindings;
}
public function setReportBindings(bool $reportBindings): self
{
$this->reportBindings = $reportBindings;
return $this;
}
public function getMaxQueries(): ?int
{
return $this->maxQueries;
}
public function setMaxQueries(?int $maxQueries): self
{
$this->maxQueries = $maxQueries;
return $this;
}
}