diff --git a/src/console/src/CommandReplacer.php b/src/console/src/CommandReplacer.php index 98ad57d08..17039e15a 100644 --- a/src/console/src/CommandReplacer.php +++ b/src/console/src/CommandReplacer.php @@ -13,6 +13,7 @@ class CommandReplacer 'name' => 'serve', 'description' => 'Start Hypervel servers', ], + 'info' => null, 'server:watch' => null, 'gen:amqp-consumer' => 'make:amqp-consumer', 'gen:amqp-producer' => 'make:amqp-producer', diff --git a/src/foundation/src/Application.php b/src/foundation/src/Application.php index 797205abe..698ef0809 100644 --- a/src/foundation/src/Application.php +++ b/src/foundation/src/Application.php @@ -203,6 +203,14 @@ public function viewPath(string $path = ''): string return $this->joinPaths($viewPath, $path); } + /** + * Get the path to the storage directory. + */ + public function storagePath(string $path = ''): string + { + return $this->joinPaths($this->basePath('storage'), $path); + } + /** * Join the given paths together. */ diff --git a/src/foundation/src/Bootstrap/RegisterFacades.php b/src/foundation/src/Bootstrap/RegisterFacades.php index ca992970a..e3de8ea4e 100644 --- a/src/foundation/src/Bootstrap/RegisterFacades.php +++ b/src/foundation/src/Bootstrap/RegisterFacades.php @@ -7,7 +7,7 @@ use Hyperf\Collection\Arr; use Hyperf\Contract\ConfigInterface; use Hypervel\Foundation\Contracts\Application as ApplicationContract; -use Hypervel\Foundation\Support\Composer; +use Hypervel\Support\Composer; use Hypervel\Support\Facades\Facade; use Throwable; diff --git a/src/foundation/src/Bootstrap/RegisterProviders.php b/src/foundation/src/Bootstrap/RegisterProviders.php index 6a065bf74..777ad425d 100644 --- a/src/foundation/src/Bootstrap/RegisterProviders.php +++ b/src/foundation/src/Bootstrap/RegisterProviders.php @@ -8,7 +8,7 @@ use Hyperf\Contract\ConfigInterface; use Hypervel\Foundation\Contracts\Application as ApplicationContract; use Hypervel\Foundation\Providers\FoundationServiceProvider; -use Hypervel\Foundation\Support\Composer; +use Hypervel\Support\Composer; use Throwable; class RegisterProviders diff --git a/src/foundation/src/ClassLoader.php b/src/foundation/src/ClassLoader.php index a3d573a01..2295c9586 100644 --- a/src/foundation/src/ClassLoader.php +++ b/src/foundation/src/ClassLoader.php @@ -10,7 +10,7 @@ use Hyperf\Di\ScanHandler\PcntlScanHandler; use Hyperf\Di\ScanHandler\ScanHandlerInterface; use Hyperf\Support\DotenvManager; -use Hypervel\Foundation\Support\Composer; +use Hypervel\Support\Composer; class ClassLoader { diff --git a/src/foundation/src/ConfigProvider.php b/src/foundation/src/ConfigProvider.php index d3bf98086..ad65fea06 100644 --- a/src/foundation/src/ConfigProvider.php +++ b/src/foundation/src/ConfigProvider.php @@ -8,6 +8,7 @@ use Hyperf\Coordinator\Listener\ResumeExitCoordinatorListener; use Hyperf\ExceptionHandler\Listener\ErrorExceptionHandler; use Hypervel\Console\ApplicationFactory; +use Hypervel\Foundation\Console\Commands\AboutCommand; use Hypervel\Foundation\Console\Commands\ServerReloadCommand; use Hypervel\Foundation\Console\Commands\VendorPublishCommand; use Hypervel\Foundation\Exceptions\Contracts\ExceptionHandler as ExceptionHandlerContract; @@ -29,6 +30,7 @@ public function __invoke(): array ReloadDotenvAndConfig::class, ], 'commands' => [ + AboutCommand::class, ServerReloadCommand::class, VendorPublishCommand::class, ], diff --git a/src/foundation/src/Console/Commands/AboutCommand.php b/src/foundation/src/Console/Commands/AboutCommand.php new file mode 100644 index 000000000..108e17bba --- /dev/null +++ b/src/foundation/src/Console/Commands/AboutCommand.php @@ -0,0 +1,191 @@ +sections(); + $information = $this->gatherApplicationInformation(); + + if ($only = $this->sections()) { + $information = array_filter($information, function ($section) use ($only) { + return in_array($this->toSearchKeyword($section), $only); + }, ARRAY_FILTER_USE_KEY); + } + + $this->display($information); + } + + /** + * Display the application information. + */ + protected function display(array $information): void + { + $this->option('json') + ? $this->displayJson($information) + : $this->displayDetail($information); + } + + /** + * Display the application information as a detail view. + */ + protected function displayDetail(array $information): void + { + foreach ($information as $section => $data) { + $this->newLine(); + + $this->components->twoColumnDetail(' ' . $section . ''); + + foreach ($data as $key => $value) { + $this->components->twoColumnDetail($key, value($value, false)); + } + } + } + + /** + * Display the application information as JSON. + */ + protected function displayJson(array $information): void + { + $output = []; + foreach ($information as $section => $data) { + $section = $this->toSearchKeyword($section); + $output[$section] = array_map(function ($value, $key) { + return [ + $this->toSearchKeyword($key) => value($value, true), + ]; + }, $data, array_keys($data)); + } + + $this->output->writeln(strip_tags(json_encode($output))); + } + + /** + * Gather information about the application. + */ + protected function gatherApplicationInformation(): array + { + $data = []; + + $formatEnabledStatus = fn ($value) => $value ? 'ENABLED' : 'OFF'; + $formatCachedStatus = fn ($value) => $value ? 'CACHED' : 'NOT CACHED'; + + $data['Environment'] = [ + 'Application Name' => $this->config->get('app.name'), + 'Hypervel Version' => $this->app->version(), /* @phpstan-ignore-line */ + 'PHP Version' => phpversion(), + 'Swoole Version' => swoole_version(), + 'Composer Version' => $this->composer->getVersion() ?? '-', + 'Environment' => $this->app->environment(), /* @phpstan-ignore-line */ + 'Debug Mode' => $this->format($this->config->get('app.debug'), console: $formatEnabledStatus), + 'URL' => Str::of($this->config->get('app.url'))->replace(['http://', 'https://'], ''), + 'Timezone' => $this->config->get('app.timezone'), + 'Locale' => $this->config->get('app.locale'), + ]; + + $data['Cache'] = [ + 'Runtime Proxy' => static::format($this->hasPhpFiles($this->app->basePath('runtime/container'), 'cache'), console: $formatCachedStatus), /* @phpstan-ignore-line */ + 'Views' => static::format($this->hasPhpFiles($this->app->storagePath('framework/views')), console: $formatCachedStatus), /* @phpstan-ignore-line */ + ]; + + $data['Drivers'] = array_filter([ + 'Broadcasting' => $this->config->get('broadcasting.default'), + 'Cache' => $this->config->get('cache.default'), + 'Database' => $this->config->get('database.default'), + 'Logs' => function ($json) { + $logChannel = $this->config->get('logging.default'); + + if ($this->config->get('logging.channels.' . $logChannel . '.driver') === 'stack') { + $secondary = new Collection($this->config->get('logging.channels.' . $logChannel . '.channels')); + + return value(static::format( + value: $logChannel, + console: fn ($value) => '' . $value . ' / ' . $secondary->implode(', '), + json: fn () => $secondary->all(), + ), $json); + } + $logs = $logChannel; + + return $logs; + }, + 'Mail' => $this->config->get('mail.default'), + 'Queue' => $this->config->get('queue.default'), + 'Session' => $this->config->get('session.driver'), + ]); + + return $data; + } + + /** + * Determine whether the given directory has PHP files. + */ + protected function hasPhpFiles(string $path, string $extension = 'php'): bool + { + return count(glob($path . "/*.{$extension}")) > 0; + } + + /** + * Get the sections provided to the command. + */ + protected function sections(): array + { + return (new Collection(explode(',', $this->option('only') ?? ''))) + ->filter() + ->map(fn ($only) => $this->toSearchKeyword($only)) + ->all(); + } + + /** + * Materialize a function that formats a given value for CLI or JSON output. + * + * @param mixed $value + * @param null|(Closure(mixed):(mixed)) $console + * @param null|(Closure(mixed):(mixed)) $json + * @return Closure(bool):mixed + */ + protected function format($value, ?Closure $console = null, ?Closure $json = null): mixed + { + return function ($isJson) use ($value, $console, $json) { + if ($isJson === true && $json instanceof Closure) { + return value($json, $value); + } + if ($isJson === false && $console instanceof Closure) { + return value($console, $value); + } + + return value($value); + }; + } + + /** + * Format the given string for searching. + */ + protected function toSearchKeyword(string $value): string + { + return (new Stringable($value))->lower()->snake()->value(); + } +} diff --git a/src/foundation/src/Contracts/Application.php b/src/foundation/src/Contracts/Application.php index 1b068235a..eaf51edcb 100644 --- a/src/foundation/src/Contracts/Application.php +++ b/src/foundation/src/Contracts/Application.php @@ -56,6 +56,11 @@ public function resourcePath(string $path = ''): string; */ public function viewPath(string $path = ''): string; + /** + * Get the path to the storage directory. + */ + public function storagePath(string $path = ''): string; + /** * Join the given paths together. */ diff --git a/src/foundation/src/Support/Composer.php b/src/support/src/Composer.php similarity index 77% rename from src/foundation/src/Support/Composer.php rename to src/support/src/Composer.php index ab348bb99..47670a93d 100644 --- a/src/foundation/src/Support/Composer.php +++ b/src/support/src/Composer.php @@ -2,11 +2,13 @@ declare(strict_types=1); -namespace Hypervel\Foundation\Support; +namespace Hypervel\Support; use Composer\Autoload\ClassLoader; use Hyperf\Collection\Collection; +use Hypervel\Filesystem\Filesystem; use RuntimeException; +use Symfony\Component\Process\Process; class Composer { @@ -181,6 +183,59 @@ public static function getBasePath(): string return static::$basePath ?: BASE_PATH; } + /** + * Get the Composer binary / command for the environment. + */ + public static function findComposer(?string $composerBinary = null): array + { + $filesystem = new Filesystem(); + if (! is_null($composerBinary) && $filesystem->exists($composerBinary)) { + return [static::phpBinary(), $composerBinary]; + } + if ($filesystem->exists(getcwd() . '/composer.phar')) { + return [static::phpBinary(), 'composer.phar']; + } + + return ['composer']; + } + + /** + * Get a new Symfony process instance. + */ + protected static function getProcess(array $command, array $env = []): Process + { + return (new Process($command, null, $env)) + ->setTimeout(null); + } + + /** + * Get the PHP binary. + */ + protected static function phpBinary(): string + { + return php_binary(); + } + + /** + * Get the version of Composer. + */ + public static function getVersion(): ?string + { + $command = array_merge(static::findComposer(), ['-V', '--no-ansi']); + + $process = static::getProcess($command); + + $process->run(); + + $output = $process->getOutput(); + + if (preg_match('/(\d+(\.\d+){2})/', $output, $version)) { + return $version[1]; + } + + return explode(' ', $output)[2] ?? null; + } + protected static function reset(): void { static::$content = null; diff --git a/tests/Foundation/Bootstrap/RegisterFacadesTest.php b/tests/Foundation/Bootstrap/RegisterFacadesTest.php index 457352644..d1db6236c 100644 --- a/tests/Foundation/Bootstrap/RegisterFacadesTest.php +++ b/tests/Foundation/Bootstrap/RegisterFacadesTest.php @@ -6,7 +6,7 @@ use Hyperf\Contract\ConfigInterface; use Hypervel\Foundation\Bootstrap\RegisterFacades; -use Hypervel\Foundation\Support\Composer; +use Hypervel\Support\Composer; use Hypervel\Tests\Foundation\Concerns\HasMockedApplication; use Hypervel\Tests\TestCase; use Mockery as m; diff --git a/tests/Foundation/Bootstrap/RegisterProvidersTest.php b/tests/Foundation/Bootstrap/RegisterProvidersTest.php index c29f3da4e..4f3eba57c 100644 --- a/tests/Foundation/Bootstrap/RegisterProvidersTest.php +++ b/tests/Foundation/Bootstrap/RegisterProvidersTest.php @@ -6,7 +6,7 @@ use Hyperf\Contract\ConfigInterface; use Hypervel\Foundation\Bootstrap\RegisterProviders; -use Hypervel\Foundation\Support\Composer; +use Hypervel\Support\Composer; use Hypervel\Support\ServiceProvider; use Hypervel\Tests\Foundation\Concerns\HasMockedApplication; use Hypervel\Tests\TestCase;