final 1.2.3 - file sh per generare zip

This commit is contained in:
2026-06-07 20:56:38 +02:00
parent f9c1268466
commit cd10d100ec
2442 changed files with 158 additions and 325423 deletions
-21
View File
@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) Taylor Otwell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-78
View File
@@ -1,78 +0,0 @@
{
"name": "laravel/agent-detector",
"description": "Detect if code is running in an AI agent or automated development environment",
"keywords": [
"php",
"ai",
"agent",
"detection",
"cursor",
"claude",
"devin",
"automation"
],
"homepage": "https://github.com/laravel/agent-detector",
"license": "MIT",
"support": {
"issues": "https://github.com/laravel/agent-detector/issues",
"source": "https://github.com/laravel/agent-detector"
},
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"require": {
"php": "^8.2.0"
},
"require-dev": {
"laravel/pint": "^1.24.0",
"pestphp/pest": "^3.8.5|^4.1.0",
"pestphp/pest-plugin-type-coverage": "^3.0|^4.0.2",
"phpstan/phpstan": "^2.1.26",
"rector/rector": "^2.1.7",
"symfony/var-dumper": "^7.3.3"
},
"autoload": {
"psr-4": {
"Laravel\\AgentDetector\\": "src/"
},
"files": [
"src/functions.php"
]
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
},
"files": [
"tests/Overrides.php"
]
},
"minimum-stability": "dev",
"prefer-stable": true,
"config": {
"sort-packages": true,
"preferred-install": "dist",
"allow-plugins": {
"pestphp/pest-plugin": true
}
},
"scripts": {
"lint": "pint",
"refactor": "rector",
"test:type-coverage": "pest --type-coverage --exactly=100",
"test:lint": "pint --test",
"test:unit": "pest",
"test:types": "phpstan",
"test:refactor": "rector --dry-run",
"test": [
"@test:lint",
"@test:type-coverage",
"@test:unit",
"@test:types",
"@test:refactor"
]
}
}
-84
View File
@@ -1,84 +0,0 @@
<?php
declare(strict_types=1);
namespace Laravel\AgentDetector;
class AgentDetector
{
public const AGENT_ENV_VARS = [
'CURSOR_AGENT' => KnownAgent::Cursor,
'GEMINI_CLI' => KnownAgent::Gemini,
'CODEX_SANDBOX' => KnownAgent::Codex,
'CODEX_CI' => KnownAgent::Codex,
'CODEX_THREAD_ID' => KnownAgent::Codex,
'AUGMENT_AGENT' => KnownAgent::AugmentCli,
'OPENCODE_CLIENT' => KnownAgent::Opencode,
'OPENCODE' => KnownAgent::Opencode,
'AMP_CURRENT_THREAD_ID' => KnownAgent::Amp,
'CLAUDECODE' => KnownAgent::Claude,
'CLAUDE_CODE' => KnownAgent::Claude,
'REPL_ID' => KnownAgent::Replit,
'COPILOT_MODEL' => KnownAgent::Copilot,
'COPILOT_ALLOW_ALL' => KnownAgent::Copilot,
'COPILOT_GITHUB_TOKEN' => KnownAgent::Copilot,
'COPILOT_CLI' => KnownAgent::Copilot,
'ANTIGRAVITY_AGENT' => KnownAgent::Antigravity,
'PI_CODING_AGENT' => KnownAgent::Pi,
'KIRO_AGENT_PATH' => KnownAgent::KiroCli,
];
public static function detect(): AgentResult
{
return self::fromAiAgentEnvVar()
?? self::fromKnownEnvVars()
?? self::fromFileSystem()
?? AgentResult::noAgent();
}
protected static function fromAiAgentEnvVar(): ?AgentResult
{
$aiAgent = getenv('AI_AGENT');
if ($aiAgent === false) {
return null;
}
$aiAgent = trim($aiAgent);
if ($aiAgent === '') {
return null;
}
return AgentResult::forAgent(match (true) {
in_array($aiAgent, ['github-copilot', 'github-copilot-cli']) => KnownAgent::Copilot,
str_starts_with($aiAgent, 'claude-code') => KnownAgent::Claude,
default => $aiAgent,
});
}
protected static function fromKnownEnvVars(): ?AgentResult
{
foreach (self::AGENT_ENV_VARS as $envVar => $agent) {
if (getenv($envVar) === false) {
continue;
}
return AgentResult::forAgent(match ($agent) {
KnownAgent::Claude => getenv('CLAUDE_CODE_IS_COWORK') !== false ? KnownAgent::Cowork : KnownAgent::Claude,
default => $agent,
});
}
return null;
}
protected static function fromFileSystem(): ?AgentResult
{
if (file_exists('/opt/.devin')) {
return AgentResult::forAgent(KnownAgent::Devin);
}
return null;
}
}
-30
View File
@@ -1,30 +0,0 @@
<?php
declare(strict_types=1);
namespace Laravel\AgentDetector;
class AgentResult
{
public readonly bool $isAgent;
public function __construct(public readonly ?string $name = null)
{
$this->isAgent = $name !== null;
}
public static function forAgent(KnownAgent|string $name): self
{
return new self($name instanceof KnownAgent ? $name->value : $name);
}
public static function noAgent(): self
{
return new self();
}
public function knownAgent(): ?KnownAgent
{
return KnownAgent::tryFrom($this->name ?? '');
}
}
-34
View File
@@ -1,34 +0,0 @@
<?php
declare(strict_types=1);
namespace Laravel\AgentDetector;
enum KnownAgent: string
{
case Cursor = 'cursor';
case Claude = 'claude';
case Cowork = 'cowork';
case Devin = 'devin';
case Replit = 'replit';
case Gemini = 'gemini';
case Codex = 'codex';
case V0 = 'v0';
case AugmentCli = 'augment-cli';
case Opencode = 'opencode';
case Amp = 'amp';
case Copilot = 'copilot';
case Antigravity = 'antigravity';
case Pi = 'pi';
case KiroCli = 'kiro-cli';
public function label(): string
{
return match ($this) {
self::AugmentCli => 'Augment CLI',
self::KiroCli => 'Kiro CLI',
self::V0 => 'v0',
default => ucfirst($this->value),
};
}
}
-10
View File
@@ -1,10 +0,0 @@
<?php
declare(strict_types=1);
namespace Laravel\AgentDetector;
function detectAgent(): AgentResult
{
return AgentDetector::detect();
}
-21
View File
@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) Taylor Otwell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-70
View File
@@ -1,70 +0,0 @@
{
"name": "laravel/pail",
"description": "Easily delve into your Laravel application's log files directly from the command line.",
"keywords": ["php", "tail", "laravel", "logs", "dev"],
"homepage": "https://github.com/laravel/pail",
"license": "MIT",
"support": {
"issues": "https://github.com/laravel/pail/issues",
"source": "https://github.com/laravel/pail"
},
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
},
{
"name": "Nuno Maduro",
"email": "enunomaduro@gmail.com"
}
],
"require": {
"php": "^8.2",
"ext-mbstring": "*",
"illuminate/console": "^10.24|^11.0|^12.0|^13.0",
"illuminate/contracts": "^10.24|^11.0|^12.0|^13.0",
"illuminate/log": "^10.24|^11.0|^12.0|^13.0",
"illuminate/process": "^10.24|^11.0|^12.0|^13.0",
"illuminate/support": "^10.24|^11.0|^12.0|^13.0",
"nunomaduro/termwind": "^1.15|^2.0",
"symfony/console": "^6.0|^7.0|^8.0"
},
"require-dev": {
"laravel/framework": "^10.24|^11.0|^12.0|^13.0",
"laravel/pint": "^1.13",
"orchestra/testbench-core": "^8.13|^9.17|^10.8|^11.0",
"pestphp/pest": "^2.20|^3.0|^4.0",
"pestphp/pest-plugin-type-coverage": "^2.3|^3.0|^4.0",
"phpstan/phpstan": "^1.12.27",
"symfony/var-dumper": "^6.3|^7.0|^8.0",
"symfony/yaml": "^6.3|^7.0|^8.0"
},
"autoload": {
"psr-4": {
"Laravel\\Pail\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"extra": {
"branch-alias": {
"dev-main": "1.x-dev"
},
"laravel": {
"providers": [
"Laravel\\Pail\\PailServiceProvider"
]
}
},
"config": {
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true
}
},
"minimum-stability": "dev",
"prefer-stable": true
}
-107
View File
@@ -1,107 +0,0 @@
<?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();
}
}
}
-13
View File
@@ -1,13 +0,0 @@
<?php
namespace Laravel\Pail\Contracts;
use Laravel\Pail\ValueObjects\MessageLogged;
interface Printer
{
/**
* Prints the given message logged.
*/
public function print(MessageLogged $messageLogged): void;
}
-100
View File
@@ -1,100 +0,0 @@
<?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
View File
@@ -1,30 +0,0 @@
<?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));
}
}
@@ -1,18 +0,0 @@
<?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.');
}
}
}
-130
View File
@@ -1,130 +0,0 @@
<?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 Illuminate\Support\Str;
use Psr\Log\LogLevel;
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;
}
if (
$messageLogged->level === LogLevel::WARNING
&& Str::contains($messageLogged->message, ['deprecated', 'Deprecated', '[\ReturnTypeWillChange]'])
) {
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());
})->toArray();
}
}
-32
View File
@@ -1,32 +0,0 @@
<?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
View File
@@ -1,76 +0,0 @@
<?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;
}
}
-81
View File
@@ -1,81 +0,0 @@
<?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\Env;
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),
$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 (bool) (Env::get('PAIL_TESTS') ?? false);
}
}
-283
View File
@@ -1,283 +0,0 @@
<?php
namespace Laravel\Pail\Printers;
use Illuminate\Support\Collection;
use Illuminate\Support\Env;
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::get('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);
if (strstr($message, PHP_EOL)) {
return "<pre>$message</pre>";
}
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()
)->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::get('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('');
}
}
-56
View File
@@ -1,56 +0,0 @@
<?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().'"';
}
}
-181
View File
@@ -1,181 +0,0 @@
<?php
namespace Laravel\Pail\ValueObjects;
use Illuminate\Support\Carbon;
use Illuminate\Support\Env;
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::get('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::get('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);
}
}
-27
View File
@@ -1,27 +0,0 @@
<?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);
}
}
-30
View File
@@ -1,30 +0,0 @@
<?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);
}
}
-33
View File
@@ -1,33 +0,0 @@
<?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);
}
}
-21
View File
@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) Taylor Otwell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-97
View File
@@ -1,97 +0,0 @@
{
"name": "laravel/pao",
"description": "Agent-optimized output for PHP testing tools",
"keywords": [
"php",
"ai",
"phpunit",
"pest",
"paratest",
"phpstan",
"agent",
"testing",
"dev"
],
"license": "MIT",
"support": {
"issues": "https://github.com/laravel/pao/issues",
"source": "https://github.com/laravel/pao"
},
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"require": {
"php": "^8.3",
"laravel/agent-detector": "^2.0.0"
},
"require-dev": {
"brianium/paratest": "^7.20.0",
"laravel/pint": "^1.29.1",
"orchestra/testbench": "^10.11.0 || ^11.1.0",
"pestphp/pest": "^4.6.3 || ^5.0.0",
"pestphp/pest-plugin-type-coverage": "^4.0.4 || ^5.0.0",
"phpstan/phpstan": "^2.1.51",
"rector/rector": "^2.4.2",
"symfony/process": "^7.4.8 || ^8.1.0",
"symfony/var-dumper": "^7.4.8 || ^8.0.8"
},
"conflict": {
"laravel/framework": "<12.0.0",
"nunomaduro/collision": "<8.9.3",
"phpunit/phpunit": "<12.5.23 || >=13.0.0 <13.1.7 || >=14.0.0",
"pestphp/pest": "<4.6.3 || >=6.0.0"
},
"autoload": {
"files": [
"src/Autoload.php"
],
"psr-4": {
"Laravel\\Pao\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"extra": {
"laravel": {
"providers": [
"Laravel\\Pao\\Laravel\\ServiceProvider"
]
},
"pest": {
"plugins": [
"Laravel\\Pao\\Drivers\\Pest\\Plugin"
]
}
},
"minimum-stability": "dev",
"prefer-stable": true,
"config": {
"sort-packages": true,
"preferred-install": "dist",
"allow-plugins": {
"pestphp/pest-plugin": true
}
},
"scripts": {
"lint": "pint",
"refactor": "rector",
"test:type-coverage": "pest --type-coverage --min=100",
"test:lint": "pint --test",
"test:unit": "pest --parallel --coverage --exactly=100",
"test:types": "phpstan",
"test:refactor": "rector --dry-run",
"test": [
"@test:refactor",
"@test:lint",
"@test:type-coverage",
"@test:types",
"@test:unit"
]
}
}
-71
View File
@@ -1,71 +0,0 @@
<?php
declare(strict_types=1);
/** @codeCoverageIgnoreStart */
namespace Laravel\Pao;
use Laravel\AgentDetector\AgentDetector;
/** @var array<int, string>|null $argv */
$argv = $_SERVER['argv'] ?? null;
if (! is_array($argv) || $argv === []) {
return;
}
if (isset($_SERVER['PAO_DISABLE'])) {
return;
}
$agent = AgentDetector::detect();
if (! $agent->isAgent) {
return;
}
if (array_intersect($argv, ['--version', '--help', '-h', 'worker'])) {
return;
}
unset($_SERVER['COLLISION_PRINTER']);
$_SERVER['PEST_PARALLEL_NO_OUTPUT'] = '1';
register_shutdown_function(function (): void {
if (! Execution::running()) {
return;
}
$execution = Execution::current();
$result = $execution->driver->parse() ?: [];
$captured = trim(UserFilters\CaptureFilter::output());
$execution->restoreStdout();
if ($captured !== '') {
$captured = OutputCleaner::clean($captured);
$lines = array_values(array_filter(
array_map(trim(...), explode("\n", $captured)),
fn (string $line): bool => $line !== ''
&& ! preg_match('/^[.st!]+$/', $line)
&& ! preg_match('/^(Tests:|Duration:|Parallel:|Time:|Generating code coverage)\s/', $line)
&& ! str_ends_with($line, 'by Sebastian Bergmann and contributors.'),
));
if ($lines !== []) {
$result['raw'] = $lines;
}
}
if ($result !== []) {
$result = ['tool' => $execution->driver->name()] + $result;
fwrite(STDOUT, json_encode($result, JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE | JSON_THROW_ON_ERROR).PHP_EOL);
}
});
Execution::start($agent, $argv);
-20
View File
@@ -1,20 +0,0 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\Contracts;
/**
* @internal
*/
interface Driver
{
public function start(): void;
public function name(): string;
/**
* @return array<string, mixed>|null
*/
public function parse(): ?array;
}
@@ -1,94 +0,0 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\Drivers\Concerns;
use PHPUnit\Event\Code\TestMethod;
use PHPUnit\Event\Telemetry\HRTime;
use PHPUnit\Event\Test\Finished;
/**
* @internal
*
* @codeCoverageIgnore
*/
final class ProfileCollector
{
private static bool $executionStarted = false;
private static ?HRTime $startTime = null;
private static float $preparedAt = 0.0;
/** @var list<array{test: string, file: string, duration_ms: int}> */
private static array $entries = [];
public static function executionStarted(): void
{
self::$executionStarted = true;
}
public static function hasExecutionStarted(): bool
{
return self::$executionStarted;
}
public static function startTimer(HRTime $time): void
{
self::$startTime = $time;
}
public static function startTimerFromNanoseconds(float $nanoseconds): void
{
$seconds = (int) ($nanoseconds / 1_000_000_000);
$nanos = (int) ($nanoseconds - ($seconds * 1_000_000_000));
self::$startTime = HRTime::fromSecondsAndNanoseconds($seconds, $nanos);
}
public static function durationMs(): int
{
if (! self::$startTime instanceof HRTime) {
return 0;
}
$startNs = (self::$startTime->seconds() * 1_000_000_000) + self::$startTime->nanoseconds();
return (int) round((hrtime(true) - $startNs) / 1_000_000);
}
public static function prepared(): void
{
self::$preparedAt = hrtime(true);
}
public static function finished(Finished $event): void
{
$test = $event->test();
$file = $test->file();
$doubleColonPos = strpos($file, '::');
if ($doubleColonPos !== false) {
$file = substr($file, 0, $doubleColonPos);
}
self::$entries[] = [
'test' => $test instanceof TestMethod ? $test->nameWithClass() : $test->id(),
'file' => $file,
'duration_ms' => self::$preparedAt > 0
? (int) round((hrtime(true) - self::$preparedAt) / 1_000_000)
: (int) round($event->telemetryInfo()->durationSincePrevious()->asFloat() * 1000),
];
self::$preparedAt = 0.0;
}
/**
* @return list<array{test: string, file: string, duration_ms: int}>
*/
public static function entries(): array
{
return self::$entries;
}
}
@@ -1,288 +0,0 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\Drivers\Concerns;
use Pest\Plugins\Parallel\Paratest\WrapperRunner;
use PHPUnit\Event\Code\TestMethod;
use PHPUnit\Event\Code\Throwable;
use PHPUnit\Event\Facade as EventFacade;
use PHPUnit\Event\Test\Errored;
use PHPUnit\Event\Test\Finished;
use PHPUnit\Event\Test\FinishedSubscriber;
use PHPUnit\Event\Test\Prepared;
use PHPUnit\Event\Test\PreparedSubscriber;
use PHPUnit\Event\TestRunner\ExecutionStarted;
use PHPUnit\Event\TestRunner\ExecutionStartedSubscriber;
use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade;
use PHPUnit\TestRunner\TestResult\Issues\Issue;
use PHPUnit\TestRunner\TestResult\TestResult;
/**
* @internal
*
* @codeCoverageIgnore
*/
trait TestResultParsable
{
public ?TestResult $testResult = null;
protected function startTimer(): void
{
try {
EventFacade::instance()->registerSubscriber(
new class implements ExecutionStartedSubscriber
{
public function notify(ExecutionStarted $event): void
{
ProfileCollector::executionStarted();
ProfileCollector::startTimer($event->telemetryInfo()->time());
}
},
);
} catch (\Throwable) {
//
}
}
protected function registerProfileSubscriber(): void
{
/** @var list<string> $argv */
$argv = $_SERVER['argv'] ?? [];
if (! in_array('--profile', $argv, true)) {
return;
}
EventFacade::instance()->registerSubscribers(
new class implements PreparedSubscriber
{
public function notify(Prepared $event): void
{
ProfileCollector::prepared();
}
},
new class implements FinishedSubscriber
{
public function notify(Finished $event): void
{
ProfileCollector::finished($event);
}
},
);
}
/**
* @return array<string, mixed>|null
*/
public function parse(): ?array
{
$testResult = $this->resolveTestResult();
if (! $testResult instanceof TestResult) {
return null;
}
if ($testResult->numberOfTestsRun() > 0 || ProfileCollector::hasExecutionStarted()) {
return $this->parseTestResult($testResult);
}
return null;
}
private function resolveTestResult(): ?TestResult
{
if ($this->testResult instanceof TestResult) {
return $this->testResult;
}
if (class_exists(WrapperRunner::class, false)
&& WrapperRunner::$result instanceof TestResult) {
return WrapperRunner::$result;
}
try {
return TestResultFacade::result();
} catch (\Throwable) {
return null;
}
}
/**
* @return array<string, mixed>
*/
private function parseTestResult(TestResult $testResult): array
{
$failedCount = $testResult->numberOfTestFailedEvents();
$erroredCount = $testResult->numberOfTestErroredEvents();
$skipped = $testResult->numberOfTestSkippedEvents() + $testResult->numberOfTestSkippedByTestSuiteSkippedEvents();
$incomplete = $testResult->numberOfTestMarkedIncompleteEvents();
$tests = $testResult->numberOfTestsRun();
$assertions = $testResult->numberOfAssertions();
$deprecations = $testResult->numberOfPhpOrUserDeprecations();
$warnings = $testResult->numberOfWarnings();
$notices = $testResult->numberOfNotices();
$risky = $testResult->numberOfTestsWithTestConsideredRiskyEvents();
$ignoredByBaseline = $testResult->numberOfIssuesIgnoredByBaseline();
$durationMs = ProfileCollector::durationMs();
/** @var list<array{test: string, file: string, line: int, message: string}> $failureDetails */
$failureDetails = [];
foreach ($testResult->testFailedEvents() as $event) {
$test = $event->test();
$throwable = $event->throwable();
$message = trim($throwable->description());
$file = $test->file();
$line = $test instanceof TestMethod ? $test->line() : 0;
[$file, $line] = $this->resolveTestLocation($file, $line, $throwable);
$failureDetails[] = [
'test' => $test instanceof TestMethod ? $test->nameWithClass() : $test->id(),
'file' => $file,
'line' => $line,
'message' => $message,
];
}
/** @var list<array{test: string, file: string, line: int, message: string}> $errorDetails */
$errorDetails = [];
foreach ($testResult->testErroredEvents() as $event) {
if ($event instanceof Errored) {
$test = $event->test();
$throwable = $event->throwable();
$message = trim($throwable->message());
$file = $test->file();
$line = $test instanceof TestMethod ? $test->line() : 0;
[$file, $line] = $this->resolveTestLocation($file, $line, $throwable);
$errorDetails[] = [
'test' => $test instanceof TestMethod ? $test->nameWithClass() : $test->id(),
'file' => $file,
'line' => $line,
'message' => $message,
];
}
}
/** @var array<string, mixed> $result */
$result = [
'result' => $testResult->wasSuccessful() ? 'passed' : 'failed',
'tests' => $tests,
'passed' => $tests - $failedCount - $erroredCount - $skipped,
'assertions' => $assertions,
'duration_ms' => $durationMs,
];
if ($failedCount > 0) {
$result['failed'] = $failedCount;
$result['failures'] = $failureDetails;
}
if ($erroredCount > 0) {
$result['errors'] = $erroredCount;
$result['error_details'] = $errorDetails;
}
if ($skipped > 0) {
$result['skipped'] = $skipped;
}
if ($incomplete > 0) {
$result['incomplete'] = $incomplete;
}
if ($deprecations > 0) {
$result['deprecations'] = $deprecations;
$result['deprecation_details'] = $this->extractIssueDetails(
[...$testResult->deprecations(), ...$testResult->phpDeprecations()],
);
}
if ($warnings > 0) {
$result['warnings'] = $warnings;
$result['warning_details'] = $this->extractIssueDetails(
[...$testResult->warnings(), ...$testResult->phpWarnings()],
);
}
if ($notices > 0) {
$result['notices'] = $notices;
$result['notice_details'] = $this->extractIssueDetails(
[...$testResult->notices(), ...$testResult->phpNotices()],
);
}
$phpErrors = $testResult->errors();
if ($phpErrors !== []) {
$result['php_errors'] = count($phpErrors);
$result['php_error_details'] = $this->extractIssueDetails($phpErrors);
}
if ($risky > 0) {
$result['risky'] = $risky;
}
if ($ignoredByBaseline > 0) {
$result['ignored_by_baseline'] = $ignoredByBaseline;
}
$profileEntries = ProfileCollector::entries();
if ($profileEntries !== []) {
usort($profileEntries, fn (array $a, array $b): int => $b['duration_ms'] <=> $a['duration_ms']);
$result['profile'] = array_slice($profileEntries, 0, 10);
}
return $result;
}
/**
* @param list<Issue> $issues
* @return list<array{file: string, line: int, message: string}>
*/
private function extractIssueDetails(array $issues): array
{
$details = [];
foreach ($issues as $issue) {
$details[] = [
'file' => $issue->file(),
'line' => $issue->line(),
'message' => $issue->description(),
];
}
return $details;
}
/**
* @return array{string, int}
*/
private function resolveTestLocation(string $file, int $line, Throwable $throwable): array
{
$isReal = $line > 0 && ! str_contains($file, "eval()'d code");
if ($isReal) {
return [$file, $line];
}
$text = $throwable->description()."\n".$throwable->stackTrace();
if (preg_match('/\bat\s+(.+\.php):(\d+)/', $text, $matches) === 1) {
return [$matches[1], (int) $matches[2]];
}
if (preg_match('#([\w/\\\\._-]+\.php):(\d+)#', $throwable->stackTrace(), $matches) === 1) {
return [$matches[1], (int) $matches[2]];
}
return [$file, $line];
}
}
-40
View File
@@ -1,40 +0,0 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\Drivers\Paratest;
use Laravel\Pao\Drivers\Concerns\TestResultParsable;
use Laravel\Pao\Drivers\Starter as BaseStarter;
/**
* @internal
*
* @codeCoverageIgnore
*/
final class Starter extends BaseStarter
{
use TestResultParsable;
public function name(): string
{
return 'paratest';
}
public function start(): void
{
$this->registerNullFilter();
$this->startTimer();
$this->silenceStdout();
/** @var list<string> $serverArgv */
$serverArgv = $_SERVER['argv'];
$argv = $serverArgv;
$argv[] = '--runner';
$argv[] = WrapperRunner::class;
$_SERVER['argv'] = $argv;
}
}
@@ -1,146 +0,0 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\Drivers\Paratest;
use Laravel\Pao\Drivers\Concerns\ProfileCollector;
use Laravel\Pao\Execution;
use ParaTest\Options;
use ParaTest\RunnerInterface;
use ParaTest\WrapperRunner\ResultPrinter;
use ParaTest\WrapperRunner\SuiteLoader;
use ParaTest\WrapperRunner\WrapperRunner as ParatestWrapperRunner;
use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade;
use PHPUnit\TestRunner\TestResult\TestResult;
use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry;
use PHPUnit\Util\ExcludeList;
use ReflectionObject;
use SplFileInfo;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
*
* @codeCoverageIgnore
*/
final readonly class WrapperRunner implements RunnerInterface
{
private ParatestWrapperRunner $runner;
public function __construct(
Options $options,
) {
$this->runner = new ParatestWrapperRunner($options, new NullOutput);
}
public function run(): int
{
$runner = $this->runner;
$r = new ReflectionObject($runner);
/** @var non-empty-string $directory */
$directory = dirname((string) $r->getFileName(), 2);
ExcludeList::addDirectory($directory);
/** @var Options $options */
$options = $r->getProperty('options')->getValue($runner);
/** @var OutputInterface $output */
$output = $r->getProperty('output')->getValue($runner);
/** @var CodeCoverageFilterRegistry $filterRegistry */
$filterRegistry = $r->getProperty('codeCoverageFilterRegistry')->getValue($runner);
$suiteLoader = new SuiteLoader($options, $output, $filterRegistry);
$result = TestResultFacade::result();
$r->getProperty('pending')->setValue($runner, $suiteLoader->tests);
/** @var ResultPrinter $printer */
$printer = $r->getProperty('printer')->getValue($runner);
$printer->setTestCount($suiteLoader->testCount);
$printer->start();
$startTime = hrtime(true);
$r->getMethod('startWorkers')->invoke($runner);
$r->getMethod('assignAllPendingTests')->invoke($runner);
$r->getMethod('waitForAllToFinish')->invoke($runner);
ProfileCollector::startTimerFromNanoseconds($startTime);
/** @var list<SplFileInfo> $testResultFiles */
$testResultFiles = $r->getProperty('testResultFiles')->getValue($runner);
$mergedResult = $this->mergeTestResults($result, $testResultFiles);
if (Execution::running()) {
$driver = Execution::current()->driver;
if ($driver instanceof Starter) {
$driver->testResult = $mergedResult;
}
}
/** @var int $exitCode */
$exitCode = $r->getMethod('complete')->invoke($runner, $result);
return $exitCode;
}
/**
* @param list<SplFileInfo> $testResultFiles
*/
private function mergeTestResults(TestResult $sum, array $testResultFiles): TestResult
{
foreach ($testResultFiles as $testResultFile) {
if (! $testResultFile->isFile()) {
continue;
}
$contents = file_get_contents($testResultFile->getPathname());
if ($contents === false) {
continue;
}
$testResult = unserialize($contents);
if (! $testResult instanceof TestResult) {
continue;
}
$sum = new TestResult(
(int) $sum->hasTests() + (int) $testResult->hasTests(),
$sum->numberOfTestsRun() + $testResult->numberOfTestsRun(),
$sum->numberOfAssertions() + $testResult->numberOfAssertions(),
[...$sum->testErroredEvents(), ...$testResult->testErroredEvents()],
[...$sum->testFailedEvents(), ...$testResult->testFailedEvents()],
array_merge_recursive($sum->testConsideredRiskyEvents(), $testResult->testConsideredRiskyEvents()), // @phpstan-ignore argument.type
[...$sum->testSuiteSkippedEvents(), ...$testResult->testSuiteSkippedEvents()],
[...$sum->testSkippedEvents(), ...$testResult->testSkippedEvents()],
[...$sum->testMarkedIncompleteEvents(), ...$testResult->testMarkedIncompleteEvents()],
array_merge_recursive($sum->testTriggeredPhpunitDeprecationEvents(), $testResult->testTriggeredPhpunitDeprecationEvents()), // @phpstan-ignore argument.type
array_merge_recursive($sum->testTriggeredPhpunitErrorEvents(), $testResult->testTriggeredPhpunitErrorEvents()), // @phpstan-ignore argument.type
array_merge_recursive($sum->testTriggeredPhpunitNoticeEvents(), $testResult->testTriggeredPhpunitNoticeEvents()), // @phpstan-ignore argument.type
array_merge_recursive($sum->testTriggeredPhpunitWarningEvents(), $testResult->testTriggeredPhpunitWarningEvents()), // @phpstan-ignore argument.type
[...$sum->testRunnerTriggeredDeprecationEvents(), ...$testResult->testRunnerTriggeredDeprecationEvents()],
[...$sum->testRunnerTriggeredNoticeEvents(), ...$testResult->testRunnerTriggeredNoticeEvents()],
[...$sum->testRunnerTriggeredWarningEvents(), ...$testResult->testRunnerTriggeredWarningEvents()],
[...$sum->errors(), ...$testResult->errors()],
[...$sum->deprecations(), ...$testResult->deprecations()],
[...$sum->notices(), ...$testResult->notices()],
[...$sum->warnings(), ...$testResult->warnings()],
[...$sum->phpDeprecations(), ...$testResult->phpDeprecations()],
[...$sum->phpNotices(), ...$testResult->phpNotices()],
[...$sum->phpWarnings(), ...$testResult->phpWarnings()],
$sum->numberOfIssuesIgnoredByBaseline() + $testResult->numberOfIssuesIgnoredByBaseline(),
);
}
return $sum;
}
}
-37
View File
@@ -1,37 +0,0 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\Drivers\Pest;
use Laravel\Pao\Execution;
use Pest\Contracts\Plugins\HandlesArguments;
/**
* @internal
*
* @codeCoverageIgnore
*/
final class Plugin implements HandlesArguments
{
public function __construct()
{
//
}
/**
* @param array<int, string> $arguments
* @return array<int, string>
*/
public function handleArguments(array $arguments): array
{
if (! Execution::running()) {
return $arguments;
}
$arguments[] = '--no-output';
$arguments[] = '--no-progress';
return $arguments;
}
}
-41
View File
@@ -1,41 +0,0 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\Drivers\Pest;
use Laravel\Pao\Drivers\Concerns\ProfileCollector;
use Laravel\Pao\Drivers\Concerns\TestResultParsable;
use Laravel\Pao\Drivers\Starter as BaseStarter;
/**
* @internal
*
* @codeCoverageIgnore
*/
final class Starter extends BaseStarter
{
use TestResultParsable;
public function name(): string
{
return 'pest';
}
public function start(): void
{
$this->registerNullFilter();
$this->startTimer();
$this->saveStdout();
$this->silenceStdout();
/** @var list<string> $argv */
$argv = $_SERVER['argv'] ?? [];
if (in_array('--parallel', $argv, true)) {
ProfileCollector::startTimerFromNanoseconds(hrtime(true));
} else {
$this->registerProfileSubscriber();
}
}
}
-208
View File
@@ -1,208 +0,0 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\Drivers\Phpstan;
use Laravel\Pao\Drivers\Starter as BaseStarter;
use Laravel\Pao\UserFilters\CaptureFilter;
/**
* @internal
*
* @codeCoverageIgnore
*/
final class Starter extends BaseStarter
{
public function name(): string
{
return 'phpstan';
}
public function start(): void
{
$this->registerNullFilter();
$this->silenceStderr();
/** @var array<int, string> $argv */
$argv = $_SERVER['argv'];
$argv = $this->ensureErrorFormatJson($argv);
$argv = $this->ensureNoProgress($argv);
$_SERVER['argv'] = $argv;
$this->silenceStdout();
}
/**
* @return array<string, mixed>|null
*/
public function parse(): ?array
{
$captured = trim(CaptureFilter::output());
CaptureFilter::reset();
if ($captured === '') {
return null;
}
$start = strpos($captured, '{');
if ($start !== false && $start > 0) {
$captured = substr($captured, $start);
}
/** @var array<string, mixed>|null $data */
$data = json_decode($captured, associative: true);
if (! is_array($data) || ! isset($data['totals'])) {
return null;
}
/** @var array<string, list<array{line: int, message: string, identifier: string, ignorable?: bool, tip?: string}>> $errorDetails */
$errorDetails = [];
$totalFileErrors = 0;
/** @var array<string, array{errors: int, messages: list<array{message: string, line: int, identifier?: string, ignorable?: bool, tip?: string}>}> $files */
$files = is_array($data['files'] ?? null) ? $data['files'] : [];
foreach ($files as $file => $fileData) {
foreach ($fileData['messages'] as $message) {
$totalFileErrors++;
$detail = [
'line' => $message['line'],
'message' => $message['message'],
'identifier' => $message['identifier'] ?? 'unknown',
];
if (isset($message['ignorable']) && $message['ignorable'] === false) {
$detail['ignorable'] = false;
}
if (isset($message['tip']) && $message['tip'] !== '') {
$detail['tip'] = $message['tip'];
}
$errorDetails[$file][] = $detail;
}
}
/** @var list<string> $errors */
$errors = is_array($data['errors'] ?? null) ? $data['errors'] : [];
/** @var list<string> $generalErrors */
$generalErrors = array_values(array_filter($errors, static fn (string $error): bool => $error !== ''));
$totalErrors = $totalFileErrors + count($generalErrors);
/** @var array<string, mixed> $result */
$result = [
'result' => $totalErrors > 0 ? 'failed' : 'passed',
'errors' => $totalErrors,
];
if ($errorDetails !== []) {
$verbose = $this->isVerbose();
$limit = 30;
if (! $verbose && $totalFileErrors > $limit) {
$result['error_details'] = $this->truncateGrouped($errorDetails, $limit);
$result['truncated'] = true;
$result['hint'] = 'Pass -v to see all errors.';
} else {
$result['error_details'] = $errorDetails;
}
}
if ($generalErrors !== []) {
$result['general_errors'] = $generalErrors;
}
return $result;
}
/**
* @param array<string, list<array{line: int, message: string, identifier: string, ignorable?: bool, tip?: string}>> $grouped
* @return array<string, list<array{line: int, message: string, identifier: string, ignorable?: bool, tip?: string}>>
*/
private function truncateGrouped(array $grouped, int $limit): array
{
$result = [];
$count = 0;
foreach ($grouped as $file => $errors) {
foreach ($errors as $error) {
if ($count >= $limit) {
break 2;
}
$result[$file][] = $error;
$count++;
}
}
return $result;
}
private function isVerbose(): bool
{
/** @var array<int, string> $argv */
$argv = $_SERVER['argv'] ?? [];
foreach ($argv as $arg) {
if (in_array($arg, ['-v', '-vv', '-vvv', '--verbose'], true)) {
return true;
}
}
return false;
}
/**
* @param array<int, string> $argv
* @return array<int, string>
*/
private function ensureErrorFormatJson(array $argv): array
{
$filtered = [];
$skipNext = false;
foreach ($argv as $arg) {
if ($skipNext) {
$skipNext = false;
continue;
}
if (str_starts_with($arg, '--error-format=')) {
continue;
}
if ($arg === '--error-format') {
$skipNext = true;
continue;
}
$filtered[] = $arg;
}
$filtered[] = '--error-format=json';
return $filtered;
}
/**
* @param array<int, string> $argv
* @return array<int, string>
*/
private function ensureNoProgress(array $argv): array
{
if (! in_array('--no-progress', $argv, true)) {
$argv[] = '--no-progress';
}
return $argv;
}
}
-41
View File
@@ -1,41 +0,0 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\Drivers\Phpunit;
use Laravel\Pao\Drivers\Concerns\TestResultParsable;
use Laravel\Pao\Drivers\Starter as BaseStarter;
/**
* @internal
*
* @codeCoverageIgnore
*/
final class Starter extends BaseStarter
{
use TestResultParsable;
public function name(): string
{
return 'phpunit';
}
public function start(): void
{
$this->registerNullFilter();
$this->startTimer();
$this->registerProfileSubscriber();
/** @var list<string> $serverArgv */
$serverArgv = $_SERVER['argv'];
$argv = $serverArgv;
if (! in_array('--no-output', $argv, true)) {
$argv[] = '--no-output';
}
$_SERVER['argv'] = $argv;
}
}
-50
View File
@@ -1,50 +0,0 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\Drivers;
use Laravel\Pao\Contracts\Driver;
use Laravel\Pao\Execution;
use Laravel\Pao\UserFilters\CaptureFilter;
use Laravel\Pao\UserFilters\NullFilter;
/**
* @internal
*
* @codeCoverageIgnore
*/
abstract class Starter implements Driver
{
protected function registerNullFilter(): void
{
if (! in_array('agent_output_null', stream_get_filters(), true)) {
stream_filter_register('agent_output_null', NullFilter::class);
}
}
protected function silenceStdout(): void
{
if (! in_array('agent_output_capture', stream_get_filters(), true)) {
stream_filter_register('agent_output_capture', CaptureFilter::class);
}
CaptureFilter::reset();
$execution = Execution::current();
$execution->filter = stream_filter_append(STDOUT, 'agent_output_capture', STREAM_FILTER_WRITE) ?: null;
}
protected function silenceStderr(): void
{
stream_filter_append(STDERR, 'agent_output_null', STREAM_FILTER_WRITE);
}
protected function saveStdout(): void
{
$execution = Execution::current();
$execution->stdout = fopen('php://stdout', 'w') ?: STDOUT;
}
}
@@ -1,20 +0,0 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\Exceptions;
use RuntimeException;
/**
* @internal
*
* @codeCoverageIgnore
*/
final class ShouldNotHappenException extends RuntimeException
{
public function __construct()
{
parent::__construct('This should not have happened. Please report this issue at [https://github.com/laravel/pao/issues/new].');
}
}
-100
View File
@@ -1,100 +0,0 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao;
use Laravel\AgentDetector\AgentResult;
use Laravel\Pao\Contracts\Driver;
use Laravel\Pao\Exceptions\ShouldNotHappenException;
use Laravel\Pao\UserFilters\CaptureFilter;
/**
* @internal
*
* @codeCoverageIgnore
*
* @phpstan-type TestDetail array{test: string, file: string, line: int, message: string}
* @phpstan-type ProfileEntry array{test: string, file: string, duration_ms: int}
* @phpstan-type Result array{result: 'passed'|'failed', tests: int, passed: int, duration_ms: int, failed?: int, failures?: list<TestDetail>, errors?: int, error_details?: list<TestDetail>, skipped?: int, profile?: list<ProfileEntry>, raw?: list<string>}
*/
final class Execution
{
private static ?self $instance = null;
/**
* @param resource|null $stdout
* @param resource|null $filter
*/
private function __construct(
public readonly AgentResult $agent,
public readonly Driver $driver,
public mixed $stdout = null,
public mixed $filter = null,
) {
//
}
/**
* @param array<int, string> $argv
*/
public static function start(AgentResult $agent, array $argv): void
{
if (self::running()) {
throw new ShouldNotHappenException;
}
$binary = basename($argv[0] ?? '');
$starter = match ($binary) {
'paratest' => new Drivers\Paratest\Starter,
'pest' => new Drivers\Pest\Starter,
'phpstan', 'phpstan.phar' => new Drivers\Phpstan\Starter,
'phpunit' => new Drivers\Phpunit\Starter,
default => null,
};
if ($starter instanceof Driver) {
self::$instance = new self(
$agent,
$starter,
);
$starter->start();
}
}
public static function running(): bool
{
return self::$instance instanceof Execution;
}
public static function current(): self
{
return self::$instance ?? throw new ShouldNotHappenException;
}
public function restoreStdout(): void
{
if (is_resource($this->filter)) {
stream_filter_remove($this->filter);
$this->filter = null;
}
}
public function flushStdout(): void
{
if (! is_resource($this->filter)) {
return;
}
$captured = CaptureFilter::output();
$this->restoreStdout();
if ($captured !== '') {
fwrite(STDOUT, $captured);
}
}
}
-62
View File
@@ -1,62 +0,0 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\Laravel;
use Illuminate\Console\OutputStyle;
use Laravel\Pao\OutputCleaner;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
*
* @codeCoverageIgnore
*/
final class PaoOutputStyle extends OutputStyle
{
private static ?OutputFormatter $formatter = null;
public function __construct(InputInterface $input, OutputInterface $output)
{
$output->setDecorated(false);
parent::__construct($input, $output);
}
/**
* @param string|iterable<string> $messages
*/
#[\Override]
public function write(string|iterable $messages, bool $newline = false, int $options = 0): void
{
parent::write($this->clean($messages), $newline, $options);
}
/**
* @param string|iterable<string> $messages
*/
#[\Override]
public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORMAL): void
{
parent::writeln($this->clean($messages), $type);
}
/**
* @param string|iterable<string> $messages
* @return string|list<string>
*/
private function clean(string|iterable $messages): string|array
{
$formatter = self::$formatter ??= new OutputFormatter(false);
$strip = fn (string $m): string => OutputCleaner::clean((string) $formatter->format($m));
if (is_string($messages)) {
return $strip($messages);
}
return array_values(array_map($strip, [...$messages]));
}
}
-46
View File
@@ -1,46 +0,0 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\Laravel;
use Illuminate\Console\Events\CommandStarting;
use Illuminate\Console\OutputStyle;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\ServiceProvider as LaravelServiceProvider;
use Laravel\AgentDetector\AgentDetector;
/**
* @internal
*
* @codeCoverageIgnore
*/
final class ServiceProvider extends LaravelServiceProvider
{
public function boot(): void
{
if (isset($_SERVER['PAO_DISABLE'])) {
return;
}
if (! $this->app->runningInConsole()) {
return;
}
if ($this->app->runningUnitTests()) {
return;
}
if (! AgentDetector::detect()->isAgent) {
return;
}
$this->app->bind(OutputStyle::class, PaoOutputStyle::class);
/** @var Dispatcher $events */
$events = $this->app->make(Dispatcher::class);
$events->listen(CommandStarting::class, function (CommandStarting $event): void {
$event->output->setDecorated(false);
});
}
}
-25
View File
@@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao;
/**
* @internal
*
* @codeCoverageIgnore
*/
final class OutputCleaner
{
public static function clean(string $output): string
{
$output = (string) preg_replace('/\e\[[0-9;]*[A-Za-z]/', '', $output);
$output = (string) preg_replace('/[\x00-\x09\x0B\x0C\x0E-\x1F\x7F]/', '', $output);
$output = (string) preg_replace('/\x{FFFD}/u', '', $output);
$output = (string) preg_replace('/[─━│┌┐└┘├┤┬┴┼▓░▒═║╔╗╚╝╠╣╦╩╬➜▶►⚠✖✔●◆■▪→←↑↓▕⨯✕]+/u', '', $output);
$output = (string) preg_replace('/\.{3,}/', '..', $output);
$output = (string) preg_replace('/[ \t]+/', ' ', $output);
return (string) preg_replace('/\n\s*\n/', "\n", $output);
}
}
-50
View File
@@ -1,50 +0,0 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\UserFilters;
use php_user_filter;
/**
* @internal
*
* @codeCoverageIgnore
*/
final class CaptureFilter extends php_user_filter
{
private static string $captured = '';
/**
* @param resource $in
* @param resource $out
* @param int $consumed
*/
public function filter($in, $out, &$consumed, bool $closing): int // @pest-ignore-type
{
while ($bucket = stream_bucket_make_writeable($in)) {
/** @var int $datalen */
$datalen = $bucket->datalen;
$consumed += $datalen;
/** @var string $data */
$data = $bucket->data;
self::$captured .= $data;
$bucket->data = '';
stream_bucket_append($out, $bucket);
}
return PSFS_PASS_ON;
}
public static function output(): string
{
return self::$captured;
}
public static function reset(): void
{
self::$captured = '';
}
}
-28
View File
@@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
namespace Laravel\Pao\UserFilters;
use php_user_filter;
/**
* @internal
*
* @codeCoverageIgnore
*/
final class NullFilter extends php_user_filter
{
public function filter($in, $out, &$consumed, bool $closing): int // @pest-ignore-type
{
while ($bucket = stream_bucket_make_writeable($in)) {
/** @var int $datalen */
$datalen = $bucket->datalen;
$consumed += $datalen;
$bucket->data = '';
stream_bucket_append($out, $bucket);
}
return PSFS_PASS_ON;
}
}
-21
View File
@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) Taylor Otwell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
BIN
View File
Binary file not shown.
-62
View File
@@ -1,62 +0,0 @@
{
"name": "laravel/pint",
"description": "An opinionated code formatter for PHP.",
"keywords": ["php", "format", "formatter", "lint", "linter", "dev"],
"homepage": "https://laravel.com",
"type": "project",
"license": "MIT",
"support": {
"issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint"
},
"authors": [
{
"name": "Nuno Maduro",
"email": "enunomaduro@gmail.com"
}
],
"require": {
"php": "^8.2.0",
"ext-json": "*",
"ext-mbstring": "*",
"ext-tokenizer": "*",
"ext-xml": "*"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.95.1",
"illuminate/view": "^12.56.0",
"larastan/larastan": "^3.9.6",
"laravel-zero/framework": "^12.1.0",
"mockery/mockery": "^1.6.12",
"nunomaduro/termwind": "^2.4.0",
"pestphp/pest": "^3.8.6",
"shipfastlabs/agent-detector": "^1.1.3"
},
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"psr-4": {
"Scripts\\": "scripts/",
"Tests\\": "tests/"
}
},
"config": {
"preferred-install": "dist",
"sort-packages": true,
"optimize-autoloader": true,
"platform": {
"php": "8.2.0"
},
"allow-plugins": {
"pestphp/pest-plugin": true
}
},
"minimum-stability": "dev",
"prefer-stable": true,
"bin": ["builds/pint"]
}
-235
View File
@@ -1,235 +0,0 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer;
use App\Fixers\TypeAnnotationsOnlyFixer;
use PhpCsFixer\ConfigurationException\InvalidFixerConfigurationException;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\Fixer\FixerInterface;
use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
use PhpCsFixer\RuleSet\RuleSetInterface;
use Symfony\Component\Finder\Finder as SymfonyFinder;
use Symfony\Component\Finder\SplFileInfo;
/**
* This overrides the default "FixerFactory" to register
* Pint's custom fixers alongside the built-in ones,
* ensuring they are available in parallel worker processes.
*
* @internal
*/
final class FixerFactory
{
private FixerNameValidator $nameValidator;
/**
* @var list<FixerInterface>
*/
private array $fixers = [];
/**
* @var array<string, FixerInterface>
*/
private array $fixersByName = [];
public function __construct()
{
$this->nameValidator = new FixerNameValidator;
}
public function setWhitespacesConfig(WhitespacesFixerConfig $config): self
{
foreach ($this->fixers as $fixer) {
if ($fixer instanceof WhitespacesAwareFixerInterface) {
$fixer->setWhitespacesConfig($config);
}
}
return $this;
}
/**
* @return list<FixerInterface>
*/
public function getFixers(): array
{
$this->fixers = Utils::sortFixers($this->fixers);
return $this->fixers;
}
/**
* @return $this
*/
public function registerBuiltInFixers(): self
{
static $builtInFixers = null;
if ($builtInFixers === null) {
/** @var list<class-string<FixerInterface>> */
$builtInFixers = [];
$finder = SymfonyFinder::create()->files()
->in(dirname(__DIR__).'/vendor/friendsofphp/php-cs-fixer/src/Fixer')
->exclude(['Internal'])
->name('*Fixer.php')
->depth(1);
/** @var SplFileInfo $file */
foreach ($finder as $file) {
$relativeNamespace = $file->getRelativePath();
$fixerClass = 'PhpCsFixer\Fixer\\'.($relativeNamespace !== '' ? $relativeNamespace.'\\' : '').$file->getBasename('.php');
$builtInFixers[] = $fixerClass;
}
}
foreach ($builtInFixers as $class) {
/** @var FixerInterface */
$fixer = new $class;
$this->registerFixer($fixer, false);
}
$this->registerCustomFixers([
new TypeAnnotationsOnlyFixer,
]);
return $this;
}
/**
* @param iterable<FixerInterface> $fixers
* @return $this
*/
public function registerCustomFixers(iterable $fixers): self
{
foreach ($fixers as $fixer) {
$this->registerFixer($fixer, true);
}
return $this;
}
/**
* @return $this
*/
public function registerFixer(FixerInterface $fixer, bool $isCustom): self
{
$name = $fixer->getName();
if (isset($this->fixersByName[$name])) {
throw new \UnexpectedValueException(\sprintf('Fixer named "%s" is already registered.', $name));
}
if (! $this->nameValidator->isValid($name, $isCustom)) {
throw new \UnexpectedValueException(\sprintf('Fixer named "%s" has invalid name.', $name));
}
$this->fixers[] = $fixer;
$this->fixersByName[$name] = $fixer;
return $this;
}
/**
* @return $this
*/
public function useRuleSet(RuleSetInterface $ruleSet): self
{
$fixers = [];
$fixersByName = [];
$fixerConflicts = [];
$fixerNames = array_keys($ruleSet->getRules());
foreach ($fixerNames as $name) {
if (! \array_key_exists($name, $this->fixersByName)) {
throw new \UnexpectedValueException(\sprintf('Rule "%s" does not exist.', $name));
}
$fixer = $this->fixersByName[$name];
$config = $ruleSet->getRuleConfiguration($name);
if ($config !== null) {
if ($fixer instanceof ConfigurableFixerInterface) {
if (\count($config) < 1) {
throw new InvalidFixerConfigurationException($fixer->getName(), 'Configuration must be an array and may not be empty.');
}
$fixer->configure($config);
} else {
throw new InvalidFixerConfigurationException($fixer->getName(), 'Is not configurable.');
}
}
$fixers[] = $fixer;
$fixersByName[$name] = $fixer;
$conflicts = array_values(array_intersect($this->getFixersConflicts($fixer), $fixerNames));
if (\count($conflicts) > 0) {
$fixerConflicts[$name] = $conflicts;
}
}
if (\count($fixerConflicts) > 0) {
throw new \UnexpectedValueException($this->generateConflictMessage($fixerConflicts));
}
$this->fixers = $fixers;
$this->fixersByName = $fixersByName;
return $this;
}
public function hasRule(string $name): bool
{
return isset($this->fixersByName[$name]);
}
/**
* @return list<string>
*/
private function getFixersConflicts(FixerInterface $fixer): array
{
return [
'blank_lines_before_namespace' => [
'no_blank_lines_before_namespace',
'single_blank_line_before_namespace',
],
'no_blank_lines_before_namespace' => ['single_blank_line_before_namespace'],
'single_import_per_statement' => ['group_import'],
][$fixer->getName()] ?? [];
}
/**
* @param array<string, list<string>> $fixerConflicts
*/
private function generateConflictMessage(array $fixerConflicts): string
{
$message = 'Rule contains conflicting fixers:';
$report = [];
foreach ($fixerConflicts as $fixer => $fixers) {
$report[$fixer] = array_values(array_filter(
$fixers,
static fn (string $candidate): bool => ! \array_key_exists($candidate, $report) || ! \in_array($fixer, $report[$candidate], true),
));
if (\count($report[$fixer]) > 0) {
$message .= \sprintf("\n- \"%s\" with %s", $fixer, Utils::naturalLanguageJoin($report[$fixer]));
}
}
return $message;
}
}
@@ -1,122 +0,0 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Runner\Parallel;
/**
* Copyright (c) 2012+ Fabien Potencier, Dariusz Rumiński
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is furnished
* to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
use Illuminate\Support\ProcessUtils;
use PhpCsFixer\Runner\RunnerConfig;
use React\EventLoop\LoopInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Process\PhpExecutableFinder;
/**
* This overrides the default "ProcessFactory" to allow for
* customization of the command-line arguments that better
* suit the needs of the laravel pint package.
*
* @author Greg Korba <greg@codito.dev>
*
* @readonly
*
* @internal
*/
final class ProcessFactory
{
public function create(
LoopInterface $loop,
InputInterface $input,
RunnerConfig $runnerConfig,
ProcessIdentifier $identifier,
int $serverPort
): Process {
$commandArgs = $this->getCommandArgs($serverPort, $identifier, $input, $runnerConfig);
return new Process(
implode(' ', $commandArgs),
$loop,
$runnerConfig->getParallelConfig()->getProcessTimeout()
);
}
/**
* @private
*
* @return list<string>
*/
public function getCommandArgs(int $serverPort, ProcessIdentifier $identifier, InputInterface $input, RunnerConfig $runnerConfig): array
{
$phpBinary = (new PhpExecutableFinder)->find(false);
if ($phpBinary === false) {
throw new ParallelisationException('Cannot find PHP executable.');
}
$mainScript = $_SERVER['argv'][0];
$commandArgs = [
ProcessUtils::escapeArgument($phpBinary),
ProcessUtils::escapeArgument($mainScript),
'worker',
'--port',
(string) $serverPort,
'--identifier',
ProcessUtils::escapeArgument($identifier->toString()),
];
if ($runnerConfig->isDryRun()) {
$commandArgs[] = '--dry-run';
}
if (filter_var($input->getOption('diff'), FILTER_VALIDATE_BOOLEAN)) {
$commandArgs[] = '--diff';
}
if (filter_var($input->getOption('stop-on-violation'), FILTER_VALIDATE_BOOLEAN)) {
$commandArgs[] = '--stop-on-violation';
}
foreach (['allow-risky', 'config', 'rules', 'using-cache', 'cache-file'] as $option) {
$optionValue = $input->getOption($option);
if ($optionValue !== null) {
$commandArgs[] = "--{$option}";
$commandArgs[] = ProcessUtils::escapeArgument($optionValue);
}
}
return $commandArgs;
}
}