refactor: susun semula struktur folder — Laravel source ke src/

This commit is contained in:
Saufi
2026-05-19 15:58:35 +08:00
parent f052251b94
commit bf53c71b45
10806 changed files with 1385379 additions and 121 deletions

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Laravel\Boost\Concerns;
use Laravel\Boost\Console\Enums\Theme;
use Laravel\Prompts\Concerns\Colors;
use function Laravel\Prompts\note;
trait DisplayHelper
{
use Colors;
protected ?Theme $theme = null;
protected function initTheme(?Theme $theme = null): void
{
$this->theme = $theme ?? Theme::random();
}
protected function displayBoostHeader(string $featureName, string $projectName, ?Theme $theme = null): void
{
$this->initTheme($theme);
$this->displayGradientLogo();
$this->displayTagline($featureName);
$this->displayNote($projectName);
}
protected function displayGradientLogo(): void
{
$lines = [
' ██████╗ ██████╗ ██████╗ ███████╗ ████████╗',
' ██╔══██╗ ██╔═══██╗ ██╔═══██╗ ██╔════╝ ╚══██╔══╝',
' ██████╔╝ ██║ ██║ ██║ ██║ ███████╗ ██║ ',
' ██╔══██╗ ██║ ██║ ██║ ██║ ╚════██║ ██║ ',
' ██████╔╝ ╚██████╔╝ ╚██████╔╝ ███████║ ██║ ',
' ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ',
];
$gradient = $this->theme->gradient();
$this->newLine();
foreach ($lines as $index => $line) {
$this->output->writeln($this->ansi256Fg($gradient[$index], $line));
}
$this->newLine();
}
protected function displayTagline(string $featureName): void
{
$tagline = " ✦ Laravel Boost :: {$featureName} :: We Must Ship ✦ ";
$this->output->writeln(' '.$this->displayBadge($tagline));
}
protected function displayNote(string $projectName): void
{
note(" Let's give {$this->displayBadge($projectName)} a Boost");
}
protected function displayOutro(string $text, string $link = '', int $terminalWidth = 80): void
{
$visibleText = preg_replace('/\x1b\[[0-9;]*m|\x1b\]8;;[^\x07]*\x07|\x1b\]8;;\x1b\\\\/', '', $text.$link) ?? '';
$visualWidth = mb_strwidth($visibleText);
$paddingLength = (int) (floor(($terminalWidth - $visualWidth) / 2)) - 2;
$padding = str_repeat(' ', max(0, $paddingLength));
$this->output->writeln(
"\e[48;5;{$this->theme->primary()}m\033[2K{$padding}\e[30m\e[1m{$text}{$link}\e[0m"
);
$this->newLine();
}
protected function ansi256Fg(int $color, string $text): string
{
return "\e[38;5;{$color}m{$text}\e[0m";
}
protected function displayBadge(string $text): string
{
return "\e[48;5;{$this->theme->primary()}m\e[30m\e[1m{$text}\e[0m";
}
protected function hyperlink(string $label, string $url): string
{
return "\033]8;;{$url}\007{$label}\033]8;;\033\\";
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Laravel\Boost\Concerns;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
trait MakesHttpRequests
{
public function client(): PendingRequest
{
$client = Http::withHeaders([
'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:140.0) Gecko/20100101 Firefox/140.0 Laravel Boost',
]);
// Disable SSL verification for local development URLs and testing
if (app()->environment(['local', 'testing']) || str_contains((string) config('boost.hosted.api_url', ''), '.test')) {
return $client->withoutVerifying();
}
return $client;
}
public function get(string $url): Response
{
return $this->client()->get($url);
}
/**
* @param array<string, mixed> $json
*/
public function json(string $url, array $json): Response
{
return $this->client()->asJson()->post($url, $json);
}
}

View File

@@ -0,0 +1,260 @@
<?php
declare(strict_types=1);
namespace Laravel\Boost\Concerns;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
trait ReadsLogs
{
/**
* Regular expression fragments and default chunk-window sizes used when
* scanning log files. Declaring them once keeps every consumer in sync.
*/
protected function getTimestampRegex(): string
{
return '\\[\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\]';
}
protected function getEntrySplitRegex(): string
{
return '/(?='.$this->getTimestampRegex().')/';
}
protected function getErrorEntryRegex(): string
{
return '/^'.$this->getTimestampRegex().'.*\\.ERROR:/';
}
protected function getChunkSizeStart(): int
{
return 64 * 1024; // 64 kB
}
protected function getChunkSizeMax(): int
{
return 1024 * 1024; // 1 MB
}
/**
* Resolve the current log file path based on Laravel's logging configuration.
*/
protected function resolveLogFilePath(): string
{
$channel = Config::get('logging.default');
$channelConfig = Config::get("logging.channels.{$channel}");
$channelConfig = $this->resolveChannelWithPath($channelConfig);
$baseLogPath = Arr::get($channelConfig, 'path', storage_path('logs/laravel.log'));
if (Arr::get($channelConfig, 'driver') === 'daily') {
return $this->resolveDailyLogFilePath($baseLogPath);
}
return $baseLogPath;
}
/**
* @param array<string, mixed>|null $channelConfig
* @return array<string, mixed>|null
*/
protected function resolveChannelWithPath(?array $channelConfig, int $depth = 0): ?array
{
if ($channelConfig === null || $depth > 2) {
return $channelConfig;
}
if (isset($channelConfig['path'])) {
return $channelConfig;
}
if (($channelConfig['driver'] ?? null) !== 'stack') {
return $channelConfig;
}
$firstValidLoggerConfig = collect($channelConfig['channels'] ?? [])
->map(fn (string $name) => Config::get("logging.channels.{$name}"))
->filter(fn ($config): bool => is_array($config))
->map(fn (array $config) => $this->resolveChannelWithPath($config, $depth + 1))
->first(fn (?array $config): bool => isset($config['path']));
return $firstValidLoggerConfig ?? $channelConfig;
}
protected function resolveDailyLogFilePath(string $basePath): string
{
$pathInfo = pathinfo($basePath);
$directory = $pathInfo['dirname'];
$filename = $pathInfo['filename'];
$extension = isset($pathInfo['extension']) ? '.'.$pathInfo['extension'] : '';
$todayLogFile = $directory.DIRECTORY_SEPARATOR.$filename.'-'.date('Y-m-d').$extension;
if (file_exists($todayLogFile)) {
return $todayLogFile;
}
$pattern = $directory.DIRECTORY_SEPARATOR.$filename.'-*'.$extension;
$files = glob($pattern) ?: [];
$datePattern = '/^'.preg_quote($filename, '/').'-\d{4}-\d{2}-\d{2}'.preg_quote($extension, '/').'$/';
$latestFile = collect($files)
->filter(fn ($file): int|false => preg_match($datePattern, basename($file)))
->sortDesc()
->first();
return $latestFile ?? $todayLogFile;
}
/**
* Determine if the given line (or entry) is an ERROR log entry.
*/
protected function isErrorEntry(string $line): bool
{
if (str_starts_with(trim($line), '{')) {
return $this->isJsonErrorEntry($line);
}
return preg_match($this->getErrorEntryRegex(), $line) === 1;
}
/**
* Retrieve the last $count complete PSR-3 log entries from the log file using
* chunked reading instead of character-by-character reverse scanning.
*
* @return string[]
*/
protected function readLastLogEntries(string $logFile, int $count): array
{
$chunkSize = $this->getChunkSizeStart();
do {
$entries = $this->scanLogChunkForEntries($logFile, $chunkSize);
if (count($entries) >= $count || $chunkSize >= $this->getChunkSizeMax()) {
break;
}
$chunkSize *= 2;
} while (true);
return array_slice($entries, -$count);
}
/**
* Return the most recent ERROR log entry, or null if none exists within the
* inspected window.
*/
protected function readLastErrorEntry(string $logFile): ?string
{
$chunkSize = $this->getChunkSizeStart();
do {
$entries = $this->scanLogChunkForEntries($logFile, $chunkSize);
for ($i = count($entries) - 1; $i >= 0; $i--) {
if ($this->isErrorEntry($entries[$i])) {
return trim((string) $entries[$i]);
}
}
if ($chunkSize >= $this->getChunkSizeMax()) {
return null;
}
$chunkSize *= 2;
} while (true);
}
/**
* Determine if the log content uses JSON format (one JSON object per line).
*/
protected function isJsonLogFormat(string $content): bool
{
$firstLine = strtok($content, "\n");
if ($firstLine === false || trim($firstLine) === '') {
return false;
}
$trimmed = trim($firstLine);
if (! str_starts_with($trimmed, '{')) {
return false;
}
json_decode($trimmed);
return json_last_error() === JSON_ERROR_NONE;
}
/**
* Determine if the given entry is a JSON-formatted ERROR log entry.
*/
protected function isJsonErrorEntry(string $entry): bool
{
$decoded = json_decode(trim($entry), true);
if (! is_array($decoded)) {
return false;
}
$level = $decoded['level'] ?? $decoded['level_name'] ?? '';
return strtoupper((string) $level) === 'ERROR' || (int) ($decoded['level'] ?? 0) >= 400;
}
/**
* Scan the last $chunkSize bytes of the log file and return an array of
* complete log entries (oldest ➜ newest).
*
* @return string[]
*/
protected function scanLogChunkForEntries(string $logFile, int $chunkSize): array
{
$fileSize = filesize($logFile);
if ($fileSize === false) {
return [];
}
$handle = fopen($logFile, 'r');
if (! $handle) {
return [];
}
try {
$offset = max($fileSize - $chunkSize, 0);
fseek($handle, $offset);
// If we started mid-line, discard the partial line to align to the next newline.
if ($offset > 0) {
fgets($handle);
}
$content = stream_get_contents($handle);
if ($this->isJsonLogFormat($content)) {
return array_values(array_filter(
explode("\n", $content),
fn (string $line): bool => trim($line) !== '',
));
}
// Split by beginning-of-entry look-ahead (PSR-3 timestamp pattern).
$entries = preg_split($this->getEntrySplitRegex(), $content, -1, PREG_SPLIT_NO_EMPTY);
if (! $entries) {
return [];
}
return $entries; // already in chronological order relative to chunk
} finally {
fclose($handle);
}
}
}

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Laravel\Boost\Concerns;
use Illuminate\Support\Facades\Blade;
use Laravel\Boost\Install\GuidelineAssist;
trait RendersBladeGuidelines
{
private array $storedSnippets = [];
protected function renderContent(string $content, string $path, array $data = []): string
{
$isBladeTemplate = str_ends_with($path, '.blade.php');
if (! $isBladeTemplate) {
return $content;
}
// Temporarily replace backticks, PHP opening tags, component tags, and Volt directives
// with placeholders before Blade processing. This prevents Blade from trying to execute
// PHP code examples, compile component references, and supports inline code.
$placeholders = [
'`' => '___SINGLE_BACKTICK___',
'<?php' => '___OPEN_PHP_TAG___',
'@volt' => '___VOLT_DIRECTIVE___',
'@endvolt' => '___ENDVOLT_DIRECTIVE___',
'</x-' => '___BLADE_COMPONENT_CLOSE___',
'<x-' => '___BLADE_COMPONENT_OPEN___',
];
$content = str_replace(array_keys($placeholders), array_values($placeholders), $content);
$rendered = Blade::render($content, [
'assist' => $this->getGuidelineAssist(),
...$data,
]);
$rendered = html_entity_decode($rendered, ENT_QUOTES | ENT_HTML5);
return str_replace(array_values($placeholders), array_keys($placeholders), $rendered);
}
protected function processBoostSnippets(string $content): string
{
return preg_replace_callback('/(?<!@)@boostsnippet\(\s*(?P<nameQuote>[\'"])(?P<name>[^\1]*?)\1(?:\s*,\s*(?P<langQuote>[\'"])(?P<lang>[^\3]*?)\3)?\s*\)(?P<content>.*?)@endboostsnippet/s', function (array $matches): string {
$name = $matches['name'];
$lang = empty($matches['lang']) ? 'html' : $matches['lang'];
$snippetContent = trim($matches['content']);
$placeholder = '___BOOST_SNIPPET_'.count($this->storedSnippets).'___';
$this->storedSnippets[$placeholder] = '<!-- '.$name.' -->'."\n".'```'.$lang."\n".$snippetContent."\n".'```'."\n\n";
return $placeholder;
}, $content);
}
protected function renderBladeFile(string $bladePath, array $data = []): string
{
if (! file_exists($bladePath)) {
return '';
}
$content = file_get_contents($bladePath);
if ($content === false) {
return '';
}
$content = $this->processBoostSnippets($content);
$rendered = $this->renderContent($content, $bladePath, $data);
$rendered = str_replace(array_keys($this->storedSnippets), array_values($this->storedSnippets), $rendered);
$this->storedSnippets = [];
return $rendered;
}
protected function getGuidelineAssist(): GuidelineAssist
{
return app(GuidelineAssist::class);
}
}