Files
proofdb/vendor/webman/console/src/Commands/MakeControllerCommand.php
T
2026-05-01 23:40:14 +08:00

283 lines
10 KiB
PHP

<?php
namespace Webman\Console\Commands;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;
use Webman\Console\Util;
use Webman\Console\Commands\Concerns\MakeCommandHelpers;
use Webman\Console\Messages;
#[AsCommand('make:controller', 'Make controller')]
class MakeControllerCommand extends Command
{
use MakeCommandHelpers;
/**
* @return void
*/
protected function configure()
{
$this->addArgument('name', InputArgument::REQUIRED, 'Controller name');
$this->addOption('plugin', 'p', InputOption::VALUE_REQUIRED, 'Plugin name under plugin/. e.g. admin');
$this->addOption('path', 'P', InputOption::VALUE_REQUIRED, 'Target directory (relative to base path). e.g. plugin/admin/app/controller');
$this->addOption('force', 'f', InputOption::VALUE_NONE, 'Override existing file without confirmation.');
$this->setHelp($this->buildHelpText());
$this->addUsage('User');
$this->addUsage('User -p admin');
$this->addUsage('User -P plugin/admin/app/controller');
$this->addUsage('admin/User -f');
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
try {
$name = Util::nameToClass((string)$input->getArgument('name'));
$plugin = $this->normalizeOptionValue($input->getOption('plugin'));
$path = $this->normalizeOptionValue($input->getOption('path'));
$force = (bool)$input->getOption('force');
if ($plugin && (str_contains($plugin, '/') || str_contains($plugin, '\\'))) {
$output->writeln($this->msg('invalid_plugin', ['{plugin}' => $plugin]));
return Command::FAILURE;
}
if ($plugin && !$this->assertPluginExists($plugin, $output)) {
return Command::FAILURE;
}
// When "-p/--plugin" is provided, controller suffix should come from the plugin config,
// not from the main project config.
if ($plugin) {
$suffix = (string)config("plugin.$plugin.app.controller_suffix", 'Controller');
} else {
$suffix = (string)config('app.controller_suffix', 'Controller');
}
$name = str_replace('\\', '/', $name);
$name = $this->applySuffixToLastSegment($name, $suffix);
// When path is not provided: in interactive mode prompt for path (same UX as make:crud).
if (!$path && $input->isInteractive()) {
$pathDefault = Util::getDefaultAppRelativePath('controller', $plugin ?: null);
$path = $this->promptForControllerPath($input, $output, $pathDefault);
}
if ($plugin || $path) {
$resolved = $this->resolveTargetByPluginOrPath(
$name,
$plugin,
$path,
$output,
fn(string $p) => Util::getDefaultAppRelativePath('controller', $p),
fn(string $key, array $replace = []) => $this->msg($key, $replace)
);
if ($resolved === null) {
return Command::FAILURE;
}
[$class, $namespace, $file] = $resolved;
if (!$this->isControllerNamespace($namespace)) {
$output->writeln(Util::selectByLocale(Messages::getPathMissingControllerMessage()));
return Command::FAILURE;
}
} else {
[$class, $namespace, $file] = $this->resolveAppControllerTarget($name);
}
$output->writeln($this->msg('make_controller', ['{name}' => $class]));
if (is_file($file) && !$force) {
$relative = $this->toRelativePath($file);
$prompt = $this->msg('override_prompt', ['{path}' => $relative]);
$question = new ConfirmationQuestion($prompt, true);
$yes = (bool)$this->askOrAbort($input, $output, $question);
if (!$yes) {
return Command::SUCCESS;
}
}
$this->createController($class, $namespace, $file);
$output->writeln($this->msg('created', ['{path}' => $this->toRelativePath($file)]));
return self::SUCCESS;
} catch (\Throwable $e) {
throw $e;
}
}
/**
* @param $name
* @param $namespace
* @param $file
* @return void
*/
protected function createController($name, $namespace, $file)
{
$path = pathinfo($file, PATHINFO_DIRNAME);
if (!is_dir($path)) {
mkdir($path, 0777, true);
}
$controller_content = <<<EOF
<?php
namespace $namespace;
use support\Request;
use support\Response;
class $name
{
public function index(Request \$request): Response
{
return response('Hello, $name');
}
}
EOF;
file_put_contents($file, $controller_content);
}
/**
* Check if namespace contains a "controller" segment (case-insensitive).
*/
protected function isControllerNamespace(string $namespace): bool
{
$segments = preg_split('/[\\\\\/]/', strtolower($namespace));
return in_array('controller', $segments, true);
}
/**
* Apply controller suffix to the last segment only.
*
* @param string $name
* @param string $suffix
* @return string
*/
protected function applySuffixToLastSegment(string $name, string $suffix): string
{
$suffix = trim($suffix);
if ($suffix === '') {
return $name;
}
$pos = strrpos($name, '/');
if ($pos === false) {
return str_ends_with($name, $suffix) ? $name : ($name . $suffix);
}
$prefix = substr($name, 0, $pos + 1);
$last = substr($name, $pos + 1);
if (!str_ends_with($last, $suffix)) {
$last .= $suffix;
}
return $prefix . $last;
}
/**
* Resolve controller namespace/file path under app/ (backward compatible).
*
* @param string $name
* @return array{0:string,1:string,2:string} [class, namespace, file]
*/
protected function resolveAppControllerTarget(string $name): array
{
$controllerRelPath = Util::getDefaultAppRelativePath('controller');
$controllerStr = Util::getDefaultAppPath('controller');
if (!($pos = strrpos($name, '/'))) {
$class = ucfirst($name);
$file = app_path() . DIRECTORY_SEPARATOR . $controllerStr . DIRECTORY_SEPARATOR . "{$class}.php";
$namespace = Util::pathToNamespace($controllerRelPath);
return [$class, $namespace, $file];
}
$nameStr = substr($name, 0, $pos);
$realNameStr = null;
if ($tmp = Util::guessPath(app_path(), $nameStr)) {
$realNameStr = $tmp;
$nameStr = $tmp;
} else if ($realSectionName = Util::guessPath(app_path(), strstr($nameStr, '/', true))) {
$upper = strtolower($realSectionName[0]) !== $realSectionName[0];
} else {
$upper = strtolower($controllerStr[0]) !== $controllerStr[0];
}
$upper = $upper ?? strtolower($nameStr[0]) !== $nameStr[0];
if ($upper && !$realNameStr) {
$nameStr = preg_replace_callback('/\/([a-z])/', static function ($matches) {
return '/' . strtoupper($matches[1]);
}, ucfirst($nameStr));
}
$appDirName = Util::detectAppDirName();
$path = "{$nameStr}/{$controllerStr}";
$class = ucfirst(substr($name, $pos + 1));
$file = app_path() . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $path) . DIRECTORY_SEPARATOR . "{$class}.php";
$namespace = str_replace('/', '\\', $appDirName . '/' . $path);
return [$class, $namespace, $file];
}
/**
* @param string $plugin
* @return string relative path
*/
protected function getPluginControllerRelativePath(string $plugin): string
{
return Util::getDefaultAppRelativePath('controller', $plugin);
}
/**
* Prompt for controller path (interactive). Reuses enter_path_prompt from make:crud messages.
*
* @param InputInterface $input
* @param OutputInterface $output
* @param string $defaultPath
* @return string
*/
protected function promptForControllerPath(InputInterface $input, OutputInterface $output, string $defaultPath): string
{
$defaultPath = $this->normalizeRelativePath($defaultPath);
$label = Util::selectLocaleMessages(Messages::getTypeLabels())['controller'] ?? 'Controller';
$promptText = Util::selectLocaleMessages(Messages::getMakeCrudMessages())['enter_path_prompt']
?? 'Enter {label} path (Enter for default: {default}): ';
$promptText = strtr($promptText, ['{label}' => $label, '{default}' => $defaultPath]);
$promptText = '<question>' . trim($promptText) . "</question>\n";
$question = new Question($promptText, $defaultPath);
$path = $this->askOrAbort($input, $output, $question);
$path = is_string($path) ? $path : $defaultPath;
return $this->normalizeRelativePath($path ?: $defaultPath);
}
/**
* Command help text (multilingual).
*
* @return string
*/
protected function buildHelpText(): string
{
return Util::selectByLocale(Messages::getMakeControllerHelpText());
}
/**
* Hardcoded CLI messages (bilingual) without translation module.
*
* @param string $key
* @param array $replace
* @return string
*/
protected function msg(string $key, array $replace = []): string
{
return strtr(Util::selectLocaleMessages(Messages::getMakeControllerMessages())[$key] ?? $key, $replace);
}
}