This commit is contained in:
2026-05-01 23:40:14 +08:00
commit b8f599a617
3867 changed files with 478663 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
build
vendor
.idea
.vscode
.phpunit*
composer.lock
+1199
View File
File diff suppressed because it is too large Load Diff
+38
View File
@@ -0,0 +1,38 @@
{
"name": "webman/console",
"type": "library",
"keywords": [
"webman console"
],
"homepage": "http://www.workerman.net",
"license": "MIT",
"description": "Webman console",
"authors": [
{
"name": "walkor",
"email": "walkor@workerman.net",
"homepage": "http://www.workerman.net",
"role": "Developer"
}
],
"support": {
"email": "walkor@workerman.net",
"issues": "https://github.com/webman-php/console/issues",
"forum": "http://www.workerman.net/questions",
"wiki": "http://www.workerman.net/doc/webman",
"source": "https://github.com/webman-php/console"
},
"require": {
"php": ">=8.1",
"symfony/console": "^6.1 || ^7.0",
"doctrine/inflector": "^2.0"
},
"autoload": {
"psr-4": {
"Webman\\Console\\" : "src"
}
},
"require-dev": {
"workerman/webman": "^2.1"
}
}
+115
View File
@@ -0,0 +1,115 @@
<?php
namespace Webman\Console;
use support\Container;
use support\Log;
use support\Request;
use Webman\App;
use Webman\Config;
use Workerman\Connection\TcpConnection;
use Workerman\Protocols\Http;
use Workerman\Worker;
use Dotenv\Dotenv;
ini_set('display_errors', 'on');
error_reporting(E_ALL);
// 为了兼容老版保留的类,后续可以删除
class Application
{
public static function run()
{
$runtime_logs_path = runtime_path() . DIRECTORY_SEPARATOR . 'logs';
if ( !file_exists($runtime_logs_path) || !is_dir($runtime_logs_path) ) {
if (!mkdir($runtime_logs_path,0777,true)) {
throw new \RuntimeException("Failed to create runtime logs directory. Please check the permission.");
}
}
$runtime_views_path = runtime_path() . DIRECTORY_SEPARATOR . 'views';
if ( !file_exists($runtime_views_path) || !is_dir($runtime_views_path) ) {
if (!mkdir($runtime_views_path,0777,true)) {
throw new \RuntimeException("Failed to create runtime views directory. Please check the permission.");
}
}
if (class_exists('Dotenv\Dotenv') && file_exists(base_path() . '/.env')) {
if (method_exists('Dotenv\Dotenv', 'createUnsafeImmutable')) {
Dotenv::createUnsafeImmutable(base_path())->load();
} else {
Dotenv::createMutable(base_path())->load();
}
}
Config::reload(config_path(), ['route', 'container']);
Worker::$onMasterReload = function (){
if (function_exists('opcache_get_status')) {
if ($status = opcache_get_status()) {
if (isset($status['scripts']) && $scripts = $status['scripts']) {
foreach (array_keys($scripts) as $file) {
opcache_invalidate($file, true);
}
}
}
}
};
$config = config('server');
Worker::$pidFile = $config['pid_file'];
Worker::$stdoutFile = $config['stdout_file'];
Worker::$logFile = $config['log_file'];
Worker::$eventLoopClass = $config['event_loop'] ?? '';
TcpConnection::$defaultMaxPackageSize = $config['max_package_size'] ?? 10*1024*1024;
if (property_exists(Worker::class, 'statusFile')) {
Worker::$statusFile = $config['status_file'] ?? '';
}
if (property_exists(Worker::class, 'stopTimeout')) {
Worker::$stopTimeout = $config['stop_timeout'] ?? 2;
}
if ($config['listen']) {
$worker = new Worker($config['listen'], $config['context']);
$property_map = [
'name',
'count',
'user',
'group',
'reusePort',
'transport',
'protocol'
];
foreach ($property_map as $property) {
if (isset($config[$property])) {
$worker->$property = $config[$property];
}
}
$worker->onWorkerStart = function ($worker) {
require_once base_path() . '/support/bootstrap.php';
$app = new App($worker, Container::instance(), Log::channel('default'), app_path(), public_path());
Http::requestClass(config('app.request_class', config('server.request_class', Request::class)));
$worker->onMessage = [$app, 'onMessage'];
};
}
// Windows does not support custom processes.
if (\DIRECTORY_SEPARATOR === '/') {
foreach (config('process', []) as $process_name => $config) {
// Remove monitor process.
if (class_exists(\Phar::class, false) && \Phar::running() && 'monitor' === $process_name) {
continue;
}
worker_start($process_name, $config);
}
foreach (config('plugin', []) as $firm => $projects) {
foreach ($projects as $name => $project) {
foreach ($project['process'] ?? [] as $process_name => $config) {
worker_start("plugin.$firm.$name.$process_name", $config);
}
}
}
}
Worker::runAll();
}
}
+73
View File
@@ -0,0 +1,73 @@
<?php
namespace Webman\Console;
use RuntimeException;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command as Commands;
use support\Container;
class Command extends Application
{
public function installInternalCommands()
{
$this->installCommands(__DIR__ . '/Commands', 'Webman\Console\Commands');
}
public function installCommands($path, $namespace = 'app\command')
{
$dir_iterator = new \RecursiveDirectoryIterator($path);
$iterator = new \RecursiveIteratorIterator($dir_iterator);
foreach ($iterator as $file) {
/** @var \SplFileInfo $file */
if (strpos($file->getFilename(), '.') === 0) {
continue;
}
if ($file->getExtension() !== 'php') {
continue;
}
// abc\def.php
$relativePath = str_replace(str_replace('/', '\\', $path . '\\'), '', str_replace('/', '\\', $file->getRealPath()));
// app\command\abc
$realNamespace = trim($namespace . '\\' . trim(dirname(str_replace('\\', DIRECTORY_SEPARATOR, $relativePath)), '.'), '\\');
$realNamespace = str_replace('/', '\\', $realNamespace);
// app\command\doc\def
$class_name = trim($realNamespace . '\\' . $file->getBasename('.php'), '\\');
if (!class_exists($class_name) || !is_a($class_name, Commands::class, true)) {
continue;
}
$this->createCommandInstance($class_name);
}
}
public function createCommandInstance($class_name)
{
$reflection = new \ReflectionClass($class_name);
if ($reflection->isAbstract()) {
return null;
}
$attributes = $reflection->getAttributes(AsCommand::class);
if (!empty($attributes)) {
$properties = current($attributes)->newInstance();
$name = $properties->name;
$description = $properties->description;
} else {
$properties = $reflection->getStaticProperties();
$name = $properties['defaultName'] ?? null;
if (!$name) {
throw new RuntimeException("Command {$class_name} has no defaultName");
}
$description = $properties['defaultDescription'] ?? null;
}
$command = Container::get($class_name);
$command->setName($name);
if ($description) {
$command->setDescription($description);
}
$this->add($command);
return $command;
}
}
@@ -0,0 +1,606 @@
<?php
namespace Webman\Console\Commands;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Webman\Console\Commands\Concerns\AppPluginCommandHelpers;
use Webman\Console\Messages;
use Webman\Console\Util;
#[AsCommand('app-plugin:create', 'Create App Plugin')]
class AppPluginCreateCommand extends Command
{
use AppPluginCommandHelpers;
/**
* @return void
*/
protected function configure()
{
$this->addArgument('name', InputArgument::REQUIRED, $this->msg('description_name'));
$this->setHelp($this->buildHelpText());
}
protected function buildHelpText(): string
{
return Util::selectByLocale(Messages::getAppPluginCreateHelpText());
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$name = $this->normalizeAppPluginName($input->getArgument('name'));
$this->writeln($output, $this->msg('create_title', ['{name}' => $name]));
if (!$this->isValidAppPluginName($name)) {
$this->writeln($output, $this->msg('bad_name', ['{name}' => $name]));
return Command::FAILURE;
}
$pluginBase = $this->appPluginBasePath($name);
if (is_dir($pluginBase)) {
$this->writeln($output, $this->msg('dir_exists', ['{path}' => $this->toRelativePath($pluginBase)]));
return Command::FAILURE;
}
try {
$this->createAll($name, $output);
} catch (\Throwable $e) {
$this->writeln($output, $this->msg('failed', ['{error}' => $e->getMessage()]));
return Command::FAILURE;
}
$this->writeln($output, $this->msg('done'));
return Command::SUCCESS;
}
/**
* @param $name
* @param OutputInterface $output
* @return void
*/
protected function createAll($name, OutputInterface $output): void
{
$base = $this->appPluginBasePath($name);
$this->mkdir($base . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'controller', 0777, true, $output);
$this->mkdir($base . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'model', 0777, true, $output);
$this->mkdir($base . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'middleware', 0777, true, $output);
$this->mkdir($base . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'view' . DIRECTORY_SEPARATOR . 'index', 0777, true, $output);
$this->mkdir($base . DIRECTORY_SEPARATOR . 'config', 0777, true, $output);
$this->mkdir($base . DIRECTORY_SEPARATOR . 'public', 0777, true, $output);
$this->mkdir($base . DIRECTORY_SEPARATOR . 'api', 0777, true, $output);
$this->createFunctionsFile($base . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'functions.php', $output);
$this->createControllerFile($base . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'controller' . DIRECTORY_SEPARATOR . 'IndexController.php', $name, $output);
$this->createViewFile($base . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'view' . DIRECTORY_SEPARATOR . 'index' . DIRECTORY_SEPARATOR . 'index.html', $output);
$this->createConfigFiles($base . DIRECTORY_SEPARATOR . 'config', $name, $output);
$this->createApiFiles($base . DIRECTORY_SEPARATOR . 'api', $name, $output);
$this->createInstallSqlFile($base . DIRECTORY_SEPARATOR . 'install.sql', $output);
}
/**
* @param $path
* @param int $mode
* @param bool $recursive
* @param OutputInterface $output
* @return void
*/
protected function mkdir($path, int $mode, bool $recursive, OutputInterface $output): void
{
if (is_dir($path)) {
return;
}
if (!mkdir($path, $mode, $recursive) && !is_dir($path)) {
throw new \RuntimeException("Unable to create directory: $path");
}
$this->writeln($output, $this->msg('created_dir', ['{path}' => $this->toRelativePath($path)]));
}
/**
* @param $path
* @param $name
* @param OutputInterface $output
* @return void
*/
protected function createControllerFile($path, $name, OutputInterface $output): void
{
$content = <<<EOF
<?php
namespace plugin\\$name\\app\\controller;
use support\\Request;
class IndexController
{
public function index()
{
return view('index/index', ['name' => '$name']);
}
}
EOF;
$this->writeFile($path, $content, $output);
}
/**
* @param $path
* @param OutputInterface $output
* @return void
*/
protected function createViewFile($path, OutputInterface $output): void
{
$content = <<<EOF
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="/favicon.ico"/>
<title>webman app plugin</title>
</head>
<body>
hello <?=htmlspecialchars(\$name)?>
</body>
</html>
EOF;
$this->writeFile($path, $content, $output);
}
/**
* @param $file
* @param OutputInterface $output
* @return void
*/
protected function createFunctionsFile($file, OutputInterface $output): void
{
$content = <<<EOF
<?php
/**
* Here is your custom functions.
*/
EOF;
$this->writeFile($file, $content, $output);
}
/**
* @param $base
* @param $name
* @param OutputInterface $output
* @return void
*/
protected function createApiFiles($base, $name, OutputInterface $output): void
{
$content = <<<EOF
<?php
namespace plugin\\$name\\api;
use plugin\admin\api\Menu;
use support\Db;
use Throwable;
class Install
{
/**
* 数据库连接
*/
protected static \$connection = 'plugin.admin.mysql';
/**
* 安装
*
* @param \$version
* @return void
*/
public static function install(\$version)
{
// 安装数据库
static::installSql();
// 导入菜单
if(\$menus = static::getMenus()) {
Menu::import(\$menus);
}
}
/**
* 卸载
*
* @param \$version
* @return void
*/
public static function uninstall(\$version)
{
// 删除菜单
foreach (static::getMenus() as \$menu) {
Menu::delete(\$menu['key']);
}
// 卸载数据库
static::uninstallSql();
}
/**
* 更新
*
* @param \$from_version
* @param \$to_version
* @param \$context
* @return void
*/
public static function update(\$from_version, \$to_version, \$context = null)
{
// 删除不用的菜单
if (isset(\$context['previous_menus'])) {
static::removeUnnecessaryMenus(\$context['previous_menus']);
}
// 安装数据库
static::installSql();
// 导入新菜单
if (\$menus = static::getMenus()) {
Menu::import(\$menus);
}
// 执行更新操作
\$update_file = __DIR__ . '/../update.php';
if (is_file(\$update_file)) {
include \$update_file;
}
}
/**
* 更新前数据收集等
*
* @param \$from_version
* @param \$to_version
* @return array|array[]
*/
public static function beforeUpdate(\$from_version, \$to_version)
{
// 在更新之前获得老菜单,通过context传递给 update
return ['previous_menus' => static::getMenus()];
}
/**
* 获取菜单
*
* @return array|mixed
*/
public static function getMenus()
{
clearstatcache();
if (is_file(\$menu_file = __DIR__ . '/../config/menu.php')) {
\$menus = include \$menu_file;
return \$menus ?: [];
}
return [];
}
/**
* 删除不需要的菜单
*
* @param \$previous_menus
* @return void
*/
public static function removeUnnecessaryMenus(\$previous_menus)
{
\$menus_to_remove = array_diff(Menu::column(\$previous_menus, 'name'), Menu::column(static::getMenus(), 'name'));
foreach (\$menus_to_remove as \$name) {
Menu::delete(\$name);
}
}
/**
* 安装SQL
*
* @return void
*/
protected static function installSql()
{
static::importSql(__DIR__ . '/../install.sql');
}
/**
* 卸载SQL
*
* @return void
*/
protected static function uninstallSql() {
// 如果卸载数据库文件存在责直接使用
\$uninstallSqlFile = __DIR__ . '/../uninstall.sql';
if (is_file(\$uninstallSqlFile)) {
static::importSql(\$uninstallSqlFile);
return;
}
// 否则根据install.sql生成卸载数据库文件uninstall.sql
\$installSqlFile = __DIR__ . '/../install.sql';
if (!is_file(\$installSqlFile)) {
return;
}
\$installSql = file_get_contents(\$installSqlFile);
preg_match_all('/CREATE TABLE `(.+?)`/si', \$installSql, \$matches);
\$dropSql = '';
foreach (\$matches[1] as \$table) {
\$dropSql .= "DROP TABLE IF EXISTS `\$table`;\\n";
}
file_put_contents(\$uninstallSqlFile, \$dropSql);
static::importSql(\$uninstallSqlFile);
unlink(\$uninstallSqlFile);
}
/**
* 导入数据库
*
* @return void
*/
public static function importSql(\$mysqlDumpFile)
{
if (!\$mysqlDumpFile || !is_file(\$mysqlDumpFile)) {
return;
}
foreach (explode(';', file_get_contents(\$mysqlDumpFile)) as \$sql) {
if (\$sql = trim(\$sql)) {
try {
Db::connection(static::\$connection)->statement(\$sql);
} catch (Throwable \$e) {}
}
}
}
}
EOF;
$this->writeFile($base . DIRECTORY_SEPARATOR . 'Install.php', $content, $output);
}
/**
* @param string $file
* @param OutputInterface $output
* @return void
*/
protected function createInstallSqlFile($file, OutputInterface $output): void
{
$this->writeFile($file, '', $output);
}
/**
* @param $base
* @param $name
* @param OutputInterface $output
* @return void
*/
protected function createConfigFiles($base, $name, OutputInterface $output): void
{
// app.php
$content = <<<EOF
<?php
use support\\Request;
return [
'debug' => true,
'controller_suffix' => 'Controller',
'controller_reuse' => false,
'version' => '1.0.0'
];
EOF;
$this->writeFile($base . DIRECTORY_SEPARATOR . 'app.php', $content, $output);
// menu.php
$content = <<<EOF
<?php
return [];
EOF;
$this->writeFile($base . DIRECTORY_SEPARATOR . 'menu.php', $content, $output);
// autoload.php
$content = <<<EOF
<?php
return [
'files' => [
base_path() . '/plugin/$name/app/functions.php',
]
];
EOF;
$this->writeFile($base . DIRECTORY_SEPARATOR . 'autoload.php', $content, $output);
// container.php
$content = <<<EOF
<?php
return new Webman\\Container;
EOF;
$this->writeFile($base . DIRECTORY_SEPARATOR . 'container.php', $content, $output);
// database.php
$content = <<<EOF
<?php
return [];
EOF;
$this->writeFile($base . DIRECTORY_SEPARATOR . 'database.php', $content, $output);
// exception.php
$content = <<<EOF
<?php
return [
'' => support\\exception\\Handler::class,
];
EOF;
$this->writeFile($base . DIRECTORY_SEPARATOR . 'exception.php', $content, $output);
// log.php
$content = <<<EOF
<?php
return [
'default' => [
'handlers' => [
[
'class' => Monolog\\Handler\\RotatingFileHandler::class,
'constructor' => [
runtime_path() . '/logs/$name.log',
7,
Monolog\\Logger::DEBUG,
],
'formatter' => [
'class' => Monolog\\Formatter\\LineFormatter::class,
'constructor' => [null, 'Y-m-d H:i:s', true],
],
]
],
],
];
EOF;
$this->writeFile($base . DIRECTORY_SEPARATOR . 'log.php', $content, $output);
// middleware.php
$content = <<<EOF
<?php
return [
'' => [
]
];
EOF;
$this->writeFile($base . DIRECTORY_SEPARATOR . 'middleware.php', $content, $output);
// process.php
$content = <<<EOF
<?php
return [];
EOF;
$this->writeFile($base . DIRECTORY_SEPARATOR . 'process.php', $content, $output);
// redis.php
$content = <<<EOF
<?php
return [
'default' => [
'host' => '127.0.0.1',
'password' => null,
'port' => 6379,
'database' => 0,
],
];
EOF;
$this->writeFile($base . DIRECTORY_SEPARATOR . 'redis.php', $content, $output);
// route.php
$content = <<<EOF
<?php
use Webman\\Route;
EOF;
$this->writeFile($base . DIRECTORY_SEPARATOR . 'route.php', $content, $output);
// static.php
$content = <<<EOF
<?php
return [
'enable' => true,
'middleware' => [], // Static file Middleware
];
EOF;
$this->writeFile($base . DIRECTORY_SEPARATOR . 'static.php', $content, $output);
// translation.php
$content = <<<EOF
<?php
return [
// Default language
'locale' => 'zh_CN',
// Fallback language
'fallback_locale' => ['zh_CN', 'en'],
// Folder where language files are stored
'path' => base_path() . "/plugin/$name/resource/translations",
];
EOF;
$this->writeFile($base . DIRECTORY_SEPARATOR . 'translation.php', $content, $output);
// view.php
$content = <<<EOF
<?php
use support\\view\\Raw;
use support\\view\\Twig;
use support\\view\\Blade;
use support\\view\\ThinkPHP;
return [
'handler' => Raw::class
];
EOF;
$this->writeFile($base . DIRECTORY_SEPARATOR . 'view.php', $content, $output);
// thinkorm.php
$content = <<<EOF
<?php
return [];
EOF;
$this->writeFile($base . DIRECTORY_SEPARATOR . 'thinkorm.php', $content, $output);
}
/**
* @param string $file
* @param string $content
* @param OutputInterface $output
* @return void
*/
protected function writeFile(string $file, string $content, OutputInterface $output): void
{
$dir = pathinfo($file, PATHINFO_DIRNAME);
if (!is_dir($dir)) {
if (!mkdir($dir, 0777, true) && !is_dir($dir)) {
throw new \RuntimeException("Unable to create directory: $dir");
}
}
if (file_put_contents($file, $content) === false) {
throw new \RuntimeException("Unable to write file: $file");
}
$this->writeln($output, $this->msg('created_file', ['{path}' => $this->toRelativePath($file)]));
}
}
@@ -0,0 +1,75 @@
<?php
namespace Webman\Console\Commands;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Webman\Console\Commands\Concerns\AppPluginCommandHelpers;
use Webman\Console\Messages;
use Webman\Console\Util;
#[AsCommand('app-plugin:install', 'Install App Plugin')]
class AppPluginInstallCommand extends Command
{
use AppPluginCommandHelpers;
/**
* @return void
*/
protected function configure()
{
$this->addArgument('name', InputArgument::REQUIRED, $this->msg('description_name'));
$this->setHelp($this->buildHelpText());
}
protected function buildHelpText(): string
{
return Util::selectByLocale(Messages::getAppPluginInstallHelpText());
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$name = $this->normalizeAppPluginName($input->getArgument('name'));
$this->writeln($output, $this->msg('install_title', ['{name}' => $name]));
if (!$this->isValidAppPluginName($name)) {
$this->writeln($output, $this->msg('bad_name', ['{name}' => $name]));
return Command::FAILURE;
}
$pluginBase = $this->appPluginBasePath($name);
if (!is_dir($pluginBase)) {
$this->writeln($output, $this->msg('plugin_not_exists', ['{path}' => $this->toRelativePath($pluginBase)]));
return Command::FAILURE;
}
$class = $this->appPluginInstallClass($name);
try {
$version = $this->appPluginVersion($name);
$this->writeln($output, $this->msg('running', [
'{class}' => $class,
'{method}' => 'install',
'{args}' => var_export([$version], true),
]));
$this->callInstallMethod($class, 'install', [$version]);
} catch (\Throwable $e) {
if ($this->isScriptMissingThrowable($e)) {
$this->writeln($output, $this->msg('script_missing', ['{class}' => $class]));
}
$this->writeln($output, $this->msg('failed', ['{error}' => $e->getMessage()]));
return Command::FAILURE;
}
$this->writeln($output, $this->msg('done'));
return Command::SUCCESS;
}
}
@@ -0,0 +1,85 @@
<?php
namespace Webman\Console\Commands;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Webman\Console\Commands\Concerns\AppPluginCommandHelpers;
use Webman\Console\Messages;
use Webman\Console\Util;
#[AsCommand('app-plugin:uninstall', 'App Plugin Uninstall')]
class AppPluginUninstallCommand extends Command
{
use AppPluginCommandHelpers;
/**
* @return void
*/
protected function configure()
{
$this->addArgument('name', InputArgument::REQUIRED, $this->msg('description_name'));
$this->addOption('yes', 'y', InputOption::VALUE_NONE, $this->msg('description_yes'));
$this->setHelp($this->buildHelpText());
}
protected function buildHelpText(): string
{
return Util::selectByLocale(Messages::getAppPluginUninstallHelpText());
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$name = $this->normalizeAppPluginName($input->getArgument('name'));
$this->writeln($output, $this->msg('uninstall_title', ['{name}' => $name]));
if (!$this->isValidAppPluginName($name)) {
$this->writeln($output, $this->msg('bad_name', ['{name}' => $name]));
return Command::FAILURE;
}
$pluginBase = $this->appPluginBasePath($name);
if (!is_dir($pluginBase)) {
$this->writeln($output, $this->msg('plugin_not_exists', ['{path}' => $this->toRelativePath($pluginBase)]));
return Command::FAILURE;
}
if (!(bool)$input->getOption('yes')) {
$question = new ConfirmationQuestion($this->msg('uninstall_confirm'), false);
if (!$this->askOrAbort($input, $output, $question)) {
return Command::SUCCESS;
}
}
$class = $this->appPluginInstallClass($name);
try {
$version = $this->appPluginVersion($name);
$this->writeln($output, $this->msg('running', [
'{class}' => $class,
'{method}' => 'uninstall',
'{args}' => var_export([$version], true),
]));
$this->callInstallMethod($class, 'uninstall', [$version]);
} catch (\Throwable $e) {
if ($this->isScriptMissingThrowable($e)) {
$this->writeln($output, $this->msg('script_missing', ['{class}' => $class]));
}
$this->writeln($output, $this->msg('failed', ['{error}' => $e->getMessage()]));
return Command::FAILURE;
}
$this->writeln($output, $this->msg('done'));
return Command::SUCCESS;
}
}
@@ -0,0 +1,97 @@
<?php
namespace Webman\Console\Commands;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Webman\Console\Commands\Concerns\AppPluginCommandHelpers;
use Webman\Console\Messages;
use Webman\Console\Util;
#[AsCommand('app-plugin:update', 'Update App Plugin')]
class AppPluginUpdateCommand extends Command
{
use AppPluginCommandHelpers;
/**
* @return void
*/
protected function configure()
{
$this->addArgument('name', InputArgument::REQUIRED, $this->msg('description_name'));
$this->addOption('from', 'f', InputOption::VALUE_REQUIRED, $this->msg('description_from'));
$this->addOption('to', 't', InputOption::VALUE_REQUIRED, $this->msg('description_to'));
$this->setHelp($this->buildHelpText());
}
protected function buildHelpText(): string
{
return Util::selectByLocale(Messages::getAppPluginUpdateHelpText());
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$name = $this->normalizeAppPluginName($input->getArgument('name'));
$this->writeln($output, $this->msg('update_title', ['{name}' => $name]));
if (!$this->isValidAppPluginName($name)) {
$this->writeln($output, $this->msg('bad_name', ['{name}' => $name]));
return Command::FAILURE;
}
$pluginBase = $this->appPluginBasePath($name);
if (!is_dir($pluginBase)) {
$this->writeln($output, $this->msg('plugin_not_exists', ['{path}' => $this->toRelativePath($pluginBase)]));
return Command::FAILURE;
}
$current = $this->appPluginVersion($name);
$from = trim((string)$input->getOption('from'));
$to = trim((string)$input->getOption('to'));
$from = $from !== '' ? $from : $current;
$to = $to !== '' ? $to : $current;
if ($from === $to) {
$this->writeln($output, $this->msg('version_same', ['{version}' => $from]));
}
$class = $this->appPluginInstallClass($name);
try {
$context = null;
if (method_exists($class, 'beforeUpdate')) {
$this->writeln($output, $this->msg('running', [
'{class}' => $class,
'{method}' => 'beforeUpdate',
'{args}' => var_export([$from, $to], true),
]));
$context = $this->callInstallMethod($class, 'beforeUpdate', [$from, $to]);
}
$this->writeln($output, $this->msg('running', [
'{class}' => $class,
'{method}' => 'update',
'{args}' => var_export([$from, $to, $context], true),
]));
$this->callInstallMethod($class, 'update', [$from, $to, $context]);
} catch (\Throwable $e) {
if ($this->isScriptMissingThrowable($e)) {
$this->writeln($output, $this->msg('script_missing', ['{class}' => $class]));
}
$this->writeln($output, $this->msg('failed', ['{error}' => $e->getMessage()]));
return Command::FAILURE;
}
$this->writeln($output, $this->msg('done'));
return Command::SUCCESS;
}
}
@@ -0,0 +1,134 @@
<?php
namespace Webman\Console\Commands;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Webman\Console\Commands\Concerns\AppPluginCommandHelpers;
use Webman\Console\Messages;
use Webman\Console\Util;
use ZipArchive;
use Exception;
use RecursiveIteratorIterator;
use RecursiveDirectoryIterator;
#[AsCommand('app-plugin:zip', 'App Plugin Zip')]
class AppPluginZipCommand extends Command
{
use AppPluginCommandHelpers;
/**
* @return void
*/
protected function configure()
{
$this->addArgument('name', InputArgument::REQUIRED, $this->msg('description_name'));
$this->setHelp($this->buildHelpText());
}
protected function buildHelpText(): string
{
return Util::selectByLocale(Messages::getAppPluginZipHelpText());
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$name = $this->normalizeAppPluginName($input->getArgument('name'));
$this->writeln($output, $this->msg('zip_title', ['{name}' => $name]));
if (!$this->isValidAppPluginName($name)) {
$this->writeln($output, $this->msg('bad_name', ['{name}' => $name]));
return Command::FAILURE;
}
$sourceDir = $this->appPluginBasePath($name);
$zipFilePath = base_path('plugin' . DIRECTORY_SEPARATOR . $name . '.zip');
if (!is_dir($sourceDir)) {
$this->writeln($output, $this->msg('plugin_not_exists', ['{path}' => $this->toRelativePath($sourceDir)]));
return Command::FAILURE;
}
if (is_file($zipFilePath)) {
if (!@unlink($zipFilePath) && is_file($zipFilePath)) {
$this->writeln($output, $this->msg('zip_delete_failed', ['{path}' => $this->toRelativePath($zipFilePath)]));
return Command::FAILURE;
}
}
$excludePaths = ['node_modules', '.git', '.idea', '.vscode', '__pycache__'];
try {
$this->zipDirectory($name, $sourceDir, $zipFilePath, $excludePaths);
} catch (\Throwable $e) {
$this->writeln($output, $this->msg('failed', ['{error}' => $e->getMessage()]));
return Command::FAILURE;
}
$this->writeln($output, $this->msg('zip_saved', ['{path}' => $this->toRelativePath($zipFilePath)]));
$this->writeln($output, $this->msg('done'));
return Command::SUCCESS;
}
/**
* @param $name
* @param $sourceDir
* @param $zipFilePath
* @param array $excludePaths
* @return bool
* @throws Exception
*/
protected function zipDirectory($name, $sourceDir, $zipFilePath, array $excludePaths = []): bool
{
$zip = new ZipArchive();
if ($zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== TRUE) {
throw new Exception($this->msg('zip_open_failed', ['{path}' => $zipFilePath]));
}
$rawSourceDir = $sourceDir;
$sourceDir = realpath($sourceDir);
if ($sourceDir === false) {
throw new Exception($this->msg('source_not_exists', ['{path}' => $rawSourceDir]));
}
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($sourceDir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($files as $file) {
if (!$file->isDir()) {
$filePath = $file->getRealPath();
// 关键修复:统一使用正斜杠 '/',避免 Windows 反斜杠污染 ZIP
$relativePath = $name . '/' . str_replace('\\', '/', substr($filePath, strlen($sourceDir) + 1));
// 修正排除目录的判断逻辑,确保所有层级都能排除
$shouldExclude = false;
foreach ($excludePaths as $excludePath) {
// 统一路径分隔符为正斜杠,兼容 Windows
$normalizedRelativePath = str_replace('\\', '/', $relativePath);
$normalizedExcludePath = str_replace('\\', '/', $excludePath);
if (preg_match('#/(?:' . preg_quote($normalizedExcludePath, '#') . ')(/|$)#i', $normalizedRelativePath)) {
$shouldExclude = true;
break;
}
}
if ($shouldExclude) {
continue;
}
$zip->addFile($filePath, $relativePath);
}
}
return $zip->close();
}
}
+153
View File
@@ -0,0 +1,153 @@
<?php
namespace Webman\Console\Commands;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use ZipArchive;
#[AsCommand('build:bin', 'build bin')]
class BuildBinCommand extends BuildPharCommand
{
protected string $binFileName;
public function __construct()
{
parent::__construct();
$this->binFileName = config('plugin.webman.console.app.bin_filename', 'webman.bin');
}
/**
* @return void
*/
protected function configure()
{
$this->addArgument('version', InputArgument::OPTIONAL, $this->msg('arg_version'));
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->checkEnv();
$output->writeln($this->msg('phar_packing'));
$version = $input->getArgument('version');
if (!$version) {
$version = (float)PHP_VERSION;
}
$version = max($version, 8.1);
$supportZip = class_exists(ZipArchive::class);
$microZipFileName = $supportZip ? "php$version.micro.sfx.zip" : "php$version.micro.sfx";
$customIni = config('plugin.webman.console.app.custom_ini', '');
$binFile = $this->buildDir. DIRECTORY_SEPARATOR . $this->binFileName;
$pharFile = $this->buildDir . DIRECTORY_SEPARATOR . $this->getPharFileName();
$zipFile = $this->buildDir. DIRECTORY_SEPARATOR . $microZipFileName;
$sfxFile = $this->buildDir. DIRECTORY_SEPARATOR . "php$version.micro.sfx";
$customIniHeaderFile = $this->buildDir. DIRECTORY_SEPARATOR . "custominiheader.bin";
// 打包
$command = new BuildPharCommand();
$command->execute($input, $output);
// 下载 micro.sfx.zip
if (!is_file($sfxFile) && !is_file($zipFile)) {
$domain = 'download.workerman.net';
$output->writeln($this->msg('downloading_php', ['{version}' => (string)$version]));
if (extension_loaded('openssl')) {
$context = stream_context_create([
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
]
]);
$client = stream_socket_client("ssl://$domain:443", $context);
} else {
$client = stream_socket_client("tcp://$domain:80");
}
if (!$client) {
$output->writeln($this->msg('download_stream_failed'));
return self::FAILURE;
}
fwrite($client, "GET /php/$microZipFileName HTTP/1.1\r\nAccept: text/html\r\nHost: $domain\r\nUser-Agent: webman/console\r\n\r\n");
$bodyLength = 0;
$bodyBuffer = '';
$lastPercent = 0;
while (true) {
$buffer = fread($client, 65535);
if ($buffer !== false) {
$bodyBuffer .= $buffer;
if (!$bodyLength && $pos = strpos($bodyBuffer, "\r\n\r\n")) {
if (!preg_match('/Content-Length: (\d+)\r\n/', $bodyBuffer, $match)) {
$output->writeln($this->msg('download_failed', ['{message}' => "php{$version}.micro.sfx.zip: missing Content-Length"]));
return self::FAILURE;
}
$firstLine = substr($bodyBuffer, 9, strpos($bodyBuffer, "\r\n") - 9);
if (!preg_match('/200 /', $bodyBuffer)) {
$output->writeln($this->msg('download_failed', ['{message}' => "php{$version}.micro.sfx.zip: {$firstLine}"]));
return self::FAILURE;
}
$bodyLength = (int)$match[1];
$bodyBuffer = substr($bodyBuffer, $pos + 4);
}
}
$receiveLength = strlen($bodyBuffer);
$percent = ceil($receiveLength * 100 / $bodyLength);
if ($percent != $lastPercent) {
echo '[' . str_pad('', $percent, '=') . '>' . str_pad('', 100 - $percent) . "$percent%]";
echo $percent < 100 ? "\r" : "\n";
}
$lastPercent = $percent;
if ($bodyLength && $receiveLength >= $bodyLength) {
file_put_contents($zipFile, $bodyBuffer);
break;
}
if ($buffer === false || !is_resource($client) || feof($client)) {
$output->writeln($this->msg('download_failed', ['{message}' => "PHP{$version}"]));
return self::FAILURE;
}
}
} else {
$output->writeln($this->msg('use_php', ['{version}' => (string)$version]));
}
// 解压
if (!is_file($sfxFile) && $supportZip) {
$zip = new ZipArchive;
$zip->open($zipFile, ZipArchive::CHECKCONS);
$zip->extractTo($this->buildDir);
}
// 生成二进制文件
file_put_contents($binFile, file_get_contents($sfxFile));
// 自定义INI
if (!empty($customIni)) {
if (file_exists($customIniHeaderFile)) {
unlink($customIniHeaderFile);
}
$f = fopen($customIniHeaderFile, 'wb');
fwrite($f, "\xfd\xf6\x69\xe6");
fwrite($f, pack('N', strlen($customIni)));
fwrite($f, $customIni);
fclose($f);
file_put_contents($binFile, file_get_contents($customIniHeaderFile),FILE_APPEND);
unlink($customIniHeaderFile);
}
file_put_contents($binFile, file_get_contents($pharFile), FILE_APPEND);
// 添加执行权限
chmod($binFile, 0755);
$output->writeln($this->msg('saved_bin', ['{name}' => $this->binFileName, '{path}' => $binFile]));
return self::SUCCESS;
}
}
+224
View File
@@ -0,0 +1,224 @@
<?php
namespace Webman\Console\Commands;
use Phar;
use RuntimeException;
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 Webman\Console\Commands\Concerns\MakeCommandHelpers;
use Webman\Console\Util;
use Webman\Console\Messages;
#[AsCommand('build:phar', 'Can be easily packaged a project into phar files. Easy to distribute and use.')]
class BuildPharCommand extends Command
{
use MakeCommandHelpers;
protected string $pharFileName;
protected string $buildDir;
protected int $pharFormat;
protected int $pharCompression;
public function __construct()
{
parent::__construct();
$this->pharFileName = config('plugin.webman.console.app.phar_filename', 'webman.phar');
$this->buildDir = rtrim(config('plugin.webman.console.app.build_dir', base_path() . '/build'), DIRECTORY_SEPARATOR);
$this->pharFormat = config('plugin.webman.console.app.phar_format', Phar::PHAR);
$this->pharCompression = config('plugin.webman.console.app.phar_compression', Phar::NONE);
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->checkEnv();
if (!file_exists($this->buildDir) && !is_dir($this->buildDir)) {
if (!mkdir($this->buildDir,0777,true)) {
throw new RuntimeException($this->err('mkdir_build_dir_failed'));
}
}
$phar_file = $this->buildDir . DIRECTORY_SEPARATOR . $this->getPharFileName();
if (file_exists($phar_file)) {
unlink($phar_file);
}
$exclude_pattern = config('plugin.webman.console.app.exclude_pattern','');
$phar = new Phar($this->buildDir . DIRECTORY_SEPARATOR . $this->pharFileName,0 , 'webman');
if(!str_ends_with($this->getPharFileName(), '.phar')) {
$phar = $phar->convertToExecutable($this->pharFormat, $this->pharCompression);
}
$phar->startBuffering();
$signature_algorithm = config('plugin.webman.console.app.signature_algorithm');
if (!in_array($signature_algorithm,[Phar::MD5, Phar::SHA1, Phar::SHA256, Phar::SHA512,Phar::OPENSSL])) {
throw new RuntimeException($this->err('bad_signature_algorithm'));
}
if ($signature_algorithm === Phar::OPENSSL) {
$private_key_file = config('plugin.webman.console.app.private_key_file');
if (!file_exists($private_key_file)) {
throw new RuntimeException($this->err('openssl_private_key_missing'));
}
$private = openssl_get_privatekey(file_get_contents($private_key_file));
$pkey = '';
openssl_pkey_export($private, $pkey);
!$phar->getSignature() && $phar->setSignatureAlgorithm($signature_algorithm, $pkey);
} else {
!$phar->getSignature() && $phar->setSignatureAlgorithm($signature_algorithm);
}
$phar->buildFromDirectory(BASE_PATH,$exclude_pattern);
$exclude_files = config('plugin.webman.console.app.exclude_files',[]);
// 打包生成的phar和bin文件是面向生产环境的,所以以下这些命令没有任何意义,执行的话甚至会出错,需要排除在外。
$exclude_command_files = [
'AppPluginCreateCommand.php',
'BuildBinCommand.php',
'BuildPharCommand.php',
'MakeBootstrapCommand.php',
'MakeCommandCommand.php',
'MakeControllerCommand.php',
'MakeMiddlewareCommand.php',
'MakeModelCommand.php',
'PluginCreateCommand.php',
'PluginDisableCommand.php',
'PluginEnableCommand.php',
'PluginExportCommand.php',
'PluginInstallCommand.php',
'PluginUninstallCommand.php'
];
$exclude_command_files = array_map(function ($cmd_file) {
return 'vendor/webman/console/src/Commands/'.$cmd_file;
},$exclude_command_files);
$exclude_files = array_unique(array_merge($exclude_command_files,$exclude_files));
foreach ($exclude_files as $file) {
if($phar->offsetExists($file)){
$phar->delete($file);
}
}
if ($this->pharCompression != Phar::NONE) {
$phar->addFromString('vendor/composer/ClassLoader.php', $this->getClassLoaderContents());
$phar->addFromString('/vendor/workerman/workerman/src/Worker.php', $this->getWorkerContents());
}
$output->writeln($this->msg('collect_complete'));
$phar->setStub("#!/usr/bin/env php
<?php
define('IN_PHAR', true);
Phar::mapPhar('webman');
require 'phar://webman/webman';
__HALT_COMPILER();
");
$output->writeln($this->msg('write_to_disk'));
$phar->stopBuffering();
unset($phar);
return self::SUCCESS;
}
protected function msg(string $key, array $replace = []): string
{
return strtr(Util::selectLocaleMessages(Messages::getBuildMessages())[$key] ?? $key, $replace);
}
/**
* Plain-text error messages for exceptions (bilingual).
*
* @param string $key
* @param array $replace
* @return string
*/
protected function err(string $key, array $replace = []): string
{
return $this->msg($key, $replace);
}
protected function getPhpIniDisplayPath(): string
{
$loaded = php_ini_loaded_file();
if (is_string($loaded) && $loaded !== '') {
return $loaded;
}
return $this->msg('ini_not_loaded');
}
/**
* @throws RuntimeException
*/
public function checkEnv(): void
{
if (!class_exists(Phar::class, false)) {
throw new RuntimeException($this->err('phar_extension_required'));
}
if (ini_get('phar.readonly')) {
$command = $this->getName();
throw new RuntimeException(
$this->err('phar_readonly_on', [
'{command}' => (string)$command,
'{ini}' => $this->getPhpIniDisplayPath(),
])
);
}
}
public function getPharFileName(): string
{
$phar_filename = $this->pharFileName;
if (empty($phar_filename)) {
throw new RuntimeException($this->err('phar_filename_required'));
}
$phar_filename .= match ($this->pharFormat) {
Phar::TAR => '.tar',
Phar::ZIP => 'zip',
default => ''
};
$phar_filename .= match ($this->pharCompression) {
Phar::GZ => '.gz',
Phar::BZ2 => '.bz2',
default => ''
};
return $phar_filename;
}
public function getClassLoaderContents(): string
{
$fileContents = file_get_contents(BASE_PATH . '/vendor/composer/ClassLoader.php');
$replaceContents = <<<'PHP'
if (str_starts_with($file, 'phar://')) {
$lockFile = sys_get_temp_dir() . '/phar_' . md5($file) . '.lock';
$fp = fopen($lockFile, 'c');
flock($fp, LOCK_EX) && include $file;
fclose($fp);
file_exists($lockFile) && @unlink($lockFile);
} else {
include $file;
}
PHP;
return str_replace(' include $file;', $replaceContents, $fileContents);
}
public function getWorkerContents(): string
{
$fileContents = file_get_contents(BASE_PATH . '/vendor/workerman/workerman/src/Worker.php');
$replaceContents = <<<'PHP'
static::forkOneWorkerForLinux($worker); php_sapi_name() == 'micro' && usleep(50000);
PHP;
return str_replace('static::forkOneWorkerForLinux($worker);', $replaceContents, $fileContents);
}
}
@@ -0,0 +1,189 @@
<?php
namespace Webman\Console\Commands\Concerns;
use Symfony\Component\Console\Output\OutputInterface;
use Webman\Console\Util;
use Webman\Console\Messages;
/**
* Helpers for AppPlugin* commands (plugin/<name>).
*/
trait AppPluginCommandHelpers
{
use MakeCommandHelpers;
/**
* Prefer admin plugin locale if available, fallback to global translation/app locale.
*
* @return string
*/
protected function getLocale(): string
{
$locale = null;
if (function_exists('config')) {
$locale = config('plugin.admin.translation.locale')
?: config('translation.locale')
?: config('app.locale');
}
$locale = is_string($locale) ? trim($locale) : '';
if ($locale === '') {
$locale = $this->resolveLocaleFromAdminTranslationConfigFile() ?? '';
}
return $locale !== '' ? $locale : Util::getLocale();
}
/**
* Resolve locale from admin plugin translation config file.
* This is a fallback when config() is not ready or the admin plugin is not loaded.
*
* @return string|null
*/
protected function resolveLocaleFromAdminTranslationConfigFile(): ?string
{
$ds = DIRECTORY_SEPARATOR;
$candidates = [
base_path('plugin' . $ds . 'admin' . $ds . 'config' . $ds . 'translation.php'),
base_path('vendor' . $ds . 'webman' . $ds . 'admin' . $ds . 'src' . $ds . 'plugin' . $ds . 'admin' . $ds . 'config' . $ds . 'translation.php'),
];
foreach ($candidates as $file) {
$config = $this->loadPhpConfigArray($file);
if ($config === null || $config === []) {
continue;
}
$locale = $config['locale'] ?? null;
$locale = is_string($locale) ? trim($locale) : '';
if ($locale !== '') {
return $locale;
}
}
return null;
}
/**
* @param mixed $value
* @return string
*/
protected function normalizeAppPluginName(mixed $value): string
{
return trim((string)$value);
}
/**
* App plugin name is a folder name under plugin/<name>.
*
* @param string $name
* @return bool
*/
protected function isValidAppPluginName(string $name): bool
{
if ($name === '') {
return false;
}
if (str_contains($name, '/') || str_contains($name, '\\')) {
return false;
}
// Keep it safe for directory/namespace usage.
return (bool)preg_match('/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/', $name);
}
/**
* @param string $name
* @return string
*/
protected function appPluginBasePath(string $name): string
{
return base_path('plugin' . DIRECTORY_SEPARATOR . $name);
}
/**
* @param string $name
* @return string
*/
protected function appPluginInstallClass(string $name): string
{
return "\\plugin\\{$name}\\api\\Install";
}
/**
* @param string $name
* @return string
*/
protected function appPluginVersion(string $name): string
{
$v = config("plugin.$name.app.version");
$v = is_string($v) ? trim($v) : '';
return $v !== '' ? $v : '1.0.0';
}
/**
* Safely call plugin Install static method with signature tolerance.
*
* @param class-string $class
* @param string $method
* @param array<int,mixed> $args
* @return mixed
*/
protected function callInstallMethod(string $class, string $method, array $args): mixed
{
if (!class_exists($class)) {
throw new \RuntimeException("Class $class not exists");
}
if (!method_exists($class, $method)) {
throw new \RuntimeException("Method $class::$method not exists");
}
$ref = new \ReflectionMethod($class, $method);
$required = $ref->getNumberOfRequiredParameters();
$total = $ref->getNumberOfParameters();
if (count($args) < $required) {
throw new \RuntimeException("Method $class::$method requires $required parameter(s)");
}
$useArgs = array_slice($args, 0, $total);
return $ref->invokeArgs(null, $useArgs);
}
/**
* @param \Throwable $e
* @return bool
*/
protected function isScriptMissingThrowable(\Throwable $e): bool
{
$msg = $e->getMessage();
return str_contains($msg, ' not exists') && (str_starts_with($msg, 'Class ') || str_starts_with($msg, 'Method '));
}
/**
* CLI messages for app plugin commands. Locale selected by getLocale() / Util fallback.
*
* @param string $key
* @param array<string,string> $replace
* @return string
*/
protected function msg(string $key, array $replace = []): string
{
$localeToMessages = Messages::getAppPluginMessages();
$messages = Util::selectLocaleMessages($localeToMessages);
$text = $messages[$key] ?? $key;
return strtr($text, $replace);
}
/**
* @param OutputInterface $output
* @param string $message
* @return void
*/
protected function writeln(OutputInterface $output, string $message): void
{
$output->writeln($message);
}
}
@@ -0,0 +1,30 @@
<?php
namespace Webman\Console\Commands\Concerns;
use Webman\Console\Util;
/**
* Base command helpers for common functionality across all command types
*/
trait BaseCommandHelpers
{
/**
* Get localized message with optional placeholder replacement
*
* @param array $messages Localized messages array
* @param string $key Message key
* @param array $replace Placeholder replacements ['{placeholder}' => 'value']
* @return mixed String message or array if message is array type
*/
protected function getLocalizedMessage(array $messages, string $key, array $replace = []): mixed
{
$text = $messages[$key] ?? $key;
if (is_array($text)) {
return $text;
}
return strtr($text, $replace);
}
}
@@ -0,0 +1,501 @@
<?php
namespace Webman\Console\Commands\Concerns;
use Webman\Console\Messages;
use Webman\Console\Util;
trait MakeCommandHelpers
{
/**
* Check whether a plugin exists by config value.
*
* The existence rule is: config("plugin.<name>") is not empty.
*
* @param string $plugin
* @return bool
*/
protected function pluginExists(string $plugin): bool
{
$plugin = trim($plugin);
if ($plugin === '') {
return false;
}
$cfg = config("plugin.$plugin");
if ($cfg === null) {
return false;
}
if (is_array($cfg)) {
return $cfg !== [];
}
if (is_string($cfg)) {
return trim($cfg) !== '';
}
return (bool)$cfg;
}
/**
* Extract plugin name from a relative path like "plugin/<name>/...".
*
* @param string|null $path
* @return string|null
*/
protected function extractPluginNameFromRelativePath(?string $path): ?string
{
$path = $this->normalizeOptionValue($path);
if (!$path) {
return null;
}
$path = $this->normalizeRelativePath($path);
if (preg_match('#^plugin/([^/]+)/#i', $path, $m)) {
$name = trim((string)($m[1] ?? ''));
return $name !== '' ? $name : null;
}
return null;
}
/**
* Validate plugin existence and output error message when missing.
*
* @param string|null $plugin
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return bool
*/
protected function assertPluginExists(?string $plugin, \Symfony\Component\Console\Output\OutputInterface $output): bool
{
$plugin = $this->normalizeOptionValue($plugin);
if (!$plugin) {
return true;
}
if ($this->pluginExists($plugin)) {
return true;
}
$output->writeln($this->renderPluginNotExistsMessage($plugin));
return false;
}
/**
* @param string $plugin
* @return string
*/
protected function renderPluginNotExistsMessage(string $plugin): string
{
$plugin = trim($plugin);
$line1 = Util::selectByLocale([
'zh_CN' => "<error>插件不存在:</error> <comment>{plugin}</comment>",
'zh_TW' => "<error>外掛不存在:</error> <comment>{plugin}</comment>",
'en' => "<error>Plugin does not exist:</error> <comment>{plugin}</comment>",
'ja' => "<error>プラグインが存在しません:</error> <comment>{plugin}</comment>",
'ko' => "<error>플러그인이 존재하지 않습니다:</error> <comment>{plugin}</comment>",
'fr' => "<error>Le plugin n'existe pas :</error> <comment>{plugin}</comment>",
'de' => "<error>Plugin existiert nicht:</error> <comment>{plugin}</comment>",
'es' => "<error>El plugin no existe:</error> <comment>{plugin}</comment>",
'pt_BR' => "<error>O plugin não existe:</error> <comment>{plugin}</comment>",
'ru' => "<error>Плагин не существует:</error> <comment>{plugin}</comment>",
'vi' => "<error>Plugin không tồn tại:</error> <comment>{plugin}</comment>",
'tr' => "<error>Eklenti mevcut değil:</error> <comment>{plugin}</comment>",
'id' => "<error>Plugin tidak ada:</error> <comment>{plugin}</comment>",
'th' => "<error>ไม่พบปลั๊กอิน:</error> <comment>{plugin}</comment>",
]);
$line2 = Util::selectByLocale([
'zh_CN' => '请检查插件名是否输入正确,或确认插件已正确安装/启用。',
'zh_TW' => '請檢查外掛名稱是否輸入正確,或確認外掛已正確安裝/啟用。',
'en' => 'Please check the plugin name, or make sure the plugin is installed/enabled.',
'ja' => 'プラグイン名を確認するか、プラグインがインストール/有効化されていることを確認してください。',
'ko' => '플러그인 이름을 확인하거나, 플러그인이 설치/활성화되었는지 확인하세요.',
'fr' => "Vérifiez le nom du plugin ou assurez-vous qu'il est installé/activé.",
'de' => 'Bitte prüfen Sie den Plugin-Namen oder ob das Plugin installiert/aktiviert ist.',
'es' => 'Compruebe el nombre del plugin o asegúrese de que esté instalado/habilitado.',
'pt_BR' => 'Verifique o nome do plugin ou se o plugin está instalado/ativado.',
'ru' => 'Проверьте имя плагина или убедитесь, что плагин установлен/включён.',
'vi' => 'Hãy kiểm tra tên plugin hoặc đảm bảo plugin đã được cài đặt/bật.',
'tr' => 'Eklenti adını kontrol edin veya eklentinin kurulu/etkin olduğundan emin olun.',
'id' => 'Periksa nama plugin atau pastikan plugin sudah terpasang/diaktifkan.',
'th' => 'โปรดตรวจสอบชื่อปลั๊กอิน หรือยืนยันว่าปลั๊กอินถูกติดตั้ง/เปิดใช้งานแล้ว',
]);
return strtr($line1, ['{plugin}' => $plugin]) . "\n" . $line2;
}
/**
* Symfony short options on some environments may return value like "=foo" for "-p=foo".
* Normalize to "foo".
*
* @param mixed $value
* @return string|null
*/
protected function normalizeOptionValue(mixed $value): ?string
{
if ($value === null) {
return null;
}
$value = trim((string)$value);
$value = ltrim($value, '=');
return $value === '' ? null : $value;
}
/**
* Normalize a relative path to use "/" separators and no leading/trailing slashes.
*
* @param string $path
* @return string
*/
protected function normalizeRelativePath(string $path): string
{
$path = trim($path);
$path = str_replace('\\', '/', $path);
$path = preg_replace('#^\\./+#', '', $path);
$path = trim($path, '/');
return $path;
}
/**
* @param string $path
* @return bool
*/
protected function isAbsolutePath(string $path): bool
{
$path = trim($path);
if ($path === '') {
return false;
}
// Windows drive letter, UNC path, or root slash.
if (preg_match('/^[a-zA-Z]:[\\\\\\/]/', $path)) {
return true;
}
if (str_starts_with($path, '\\\\') || str_starts_with($path, '//')) {
return true;
}
return str_starts_with($path, '/') || str_starts_with($path, '\\');
}
/**
* Compare two relative paths on Windows-friendly rules.
*
* @param string $a
* @param string $b
* @return bool
*/
protected function pathsEqual(string $a, string $b): bool
{
$a = strtolower($this->normalizeRelativePath($a));
$b = strtolower($this->normalizeRelativePath($b));
return $a === $b;
}
/**
* Convert an absolute path to a workspace-relative path for nicer CLI output.
*
* @param string $path
* @return string
*/
protected function toRelativePath(string $path): string
{
$base = base_path();
$baseNorm = rtrim(str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $base), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
$pathNorm = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path);
if (str_starts_with(strtolower($pathNorm), strtolower($baseNorm))) {
$rel = substr($pathNorm, strlen($baseNorm));
} else {
$rel = $pathNorm;
}
// Use forward slashes for nicer CLI output.
return str_replace(DIRECTORY_SEPARATOR, '/', $rel);
}
/**
* Get current locale for CLI messages. Delegates to Util::getLocale().
*
* @return string
*/
protected function getLocale(): string
{
return Util::getLocale();
}
/**
* Ask a question with Ctrl+C safety.
*
* On Windows, Ctrl+C during a prompt can corrupt the console code page,
* causing subsequent UTF-8 output to display as garbled text.
* This method restores the code page after every prompt to prevent that.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @param \Symfony\Component\Console\Question\Question $question
* @return mixed
*/
protected function askOrAbort(
\Symfony\Component\Console\Input\InputInterface $input,
\Symfony\Component\Console\Output\OutputInterface $output,
\Symfony\Component\Console\Question\Question $question
): mixed {
$cpBefore = function_exists('sapi_windows_cp_get') ? sapi_windows_cp_get() : null;
/** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */
$helper = $this->getHelper('question');
try {
$value = $helper->ask($input, $output, $question);
} catch (\Throwable $e) {
$this->restoreConsoleCodePage($cpBefore);
throw $e;
}
$this->restoreConsoleCodePage($cpBefore);
if ($value === null || (is_string($value) && str_contains($value, "\x03"))) {
exit(130);
}
return $value;
}
/**
* Restore console code page on Windows after an interactive prompt.
* Ctrl+C can corrupt the code page; this resets it to what it was before.
*/
private function restoreConsoleCodePage(?int $codepage): void
{
if ($codepage !== null && function_exists('sapi_windows_cp_set')) {
sapi_windows_cp_set($codepage);
}
}
/**
* Resolve namespace/file path by --plugin/-p or --path/-P.
* - --path/-P: must be a relative path (to project root).
* - If both are provided, they must point to the same directory, otherwise it's an error.
*
* @param string $name Name like "admin/User"
* @param string|null $plugin
* @param string|null $path
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @param callable(string):string $pluginDefaultPathResolver Returns relative dir like "plugin/admin/app/controller"
* @param callable(string,array<string,string>):string $msg Message resolver: fn($key,$replace)=>string
* @return array{0:string,1:string,2:string}|null [class, namespace, file]
*/
protected function resolveTargetByPluginOrPath(
string $name,
?string $plugin,
?string $path,
\Symfony\Component\Console\Output\OutputInterface $output,
callable $pluginDefaultPathResolver,
callable $msg
): ?array {
$pathNorm = $path ? $this->normalizeRelativePath($path) : null;
if ($pathNorm !== null && $this->isAbsolutePath($pathNorm)) {
$output->writeln($msg('invalid_path', ['{path}' => (string)$path]));
return null;
}
// Validate plugin existence (from --plugin/-p or inferred from --path/-P).
$pluginToCheck = $this->normalizeOptionValue($plugin) ?: $this->extractPluginNameFromRelativePath($pathNorm);
if ($pluginToCheck && !$this->assertPluginExists($pluginToCheck, $output)) {
return null;
}
$expected = null;
if ($plugin) {
$expected = $pluginDefaultPathResolver($plugin);
}
if ($plugin && $pathNorm) {
$pluginPrefix = 'plugin/' . $plugin . '/';
if (!str_starts_with(strtolower($pathNorm), strtolower($pluginPrefix))) {
$output->writeln(strtr(
Util::selectByLocale(Messages::getPluginPathConflictMessage()),
['{plugin}' => $plugin, '{path}' => $pathNorm]
));
return null;
}
}
$targetRel = $pathNorm ?: $expected;
if (!$targetRel) {
return null;
}
$targetDir = base_path($targetRel);
$namespaceRoot = trim(str_replace('/', '\\', $targetRel), '\\');
if (!($pos = strrpos($name, '/'))) {
$class = ucfirst($name);
$subPath = '';
} else {
$subPath = substr($name, 0, $pos);
$class = ucfirst(substr($name, $pos + 1));
}
$subDir = $subPath ? str_replace('/', DIRECTORY_SEPARATOR, $subPath) . DIRECTORY_SEPARATOR : '';
$file = $targetDir . DIRECTORY_SEPARATOR . $subDir . $class . '.php';
$namespace = $namespaceRoot . ($subPath ? '\\' . str_replace('/', '\\', $subPath) : '');
return [$class, $namespace, $file];
}
/**
* Load a php config file (return array).
*
* @param string $file
* @return array|null null when file exists but does not return an array or cannot be included
*/
protected function loadPhpConfigArray(string $file): ?array
{
if (!is_file($file)) {
return [];
}
try {
$data = include $file;
} catch (\Throwable) {
return null;
}
return is_array($data) ? $data : null;
}
/**
* Get a simple PHP header, preserving the top docblock if present.
*
* @param string $file
* @return string
*/
protected function getPhpHeaderWithDocblock(string $file): string
{
$default = "<?php\n\n";
if (!is_file($file)) {
return $default;
}
$content = file_get_contents($file);
if (!is_string($content) || $content === '') {
return $default;
}
if (preg_match('/\A<\?php\s*(\/\*\*[\s\S]*?\*\/\s*)/i', $content, $m)) {
$doc = rtrim($m[1]) . "\n\n";
return "<?php\n" . $doc;
}
return $default;
}
/**
* Ensure parent directory exists.
*
* @param string $file
* @return void
*/
protected function ensureParentDir(string $file): void
{
$dir = pathinfo($file, PATHINFO_DIRNAME);
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
}
}
/**
* Add a class to a flat config list (return [ClassA::class, ...]).
*
* @param string $file
* @param string $classFqn e.g. app\bootstrap\Test
* @return bool changed or not
*/
protected function addClassToFlatClassListConfig(string $file, string $classFqn): bool
{
$config = $this->loadPhpConfigArray($file);
if ($config === null) {
return false;
}
$classFqn = ltrim(trim($classFqn), '\\');
if (in_array($classFqn, $config, true)) {
return false;
}
$config[] = $classFqn;
$this->ensureParentDir($file);
$header = $this->getPhpHeaderWithDocblock($file);
$body = $this->renderFlatClassListConfig($config);
file_put_contents($file, $header . $body);
return true;
}
/**
* Add a class to middleware config under empty key ''.
* Return value format:
* return [
* '' => [
* Foo::class,
* ],
* ];
*
* @param string $file
* @param string $classFqn e.g. app\middleware\StaticFile
* @return bool changed or not
*/
protected function addClassToMiddlewareConfig(string $file, string $classFqn): bool
{
$config = $this->loadPhpConfigArray($file);
if ($config === null) {
return false;
}
$classFqn = ltrim(trim($classFqn), '\\');
if (!array_key_exists('', $config)) {
// Put '' first for readability.
$config = ['' => []] + $config;
}
$list = $config[''];
if (!is_array($list)) {
$list = [];
}
if (in_array($classFqn, $list, true)) {
return false;
}
$list[] = $classFqn;
$config[''] = $list;
$this->ensureParentDir($file);
$header = $this->getPhpHeaderWithDocblock($file);
$body = $this->renderMiddlewareConfig($config);
file_put_contents($file, $header . $body);
return true;
}
/**
* @param array<int,string> $classes
* @return string
*/
protected function renderFlatClassListConfig(array $classes): string
{
$lines = [];
$lines[] = "return [";
foreach ($classes as $c) {
$c = ltrim((string)$c, '\\');
if ($c === '') {
continue;
}
$lines[] = " {$c}::class,";
}
$lines[] = "];\n";
return implode("\n", $lines);
}
/**
* @param array<string,mixed> $config
* @return string
*/
protected function renderMiddlewareConfig(array $config): string
{
$lines = [];
$lines[] = "return [";
foreach ($config as $key => $value) {
$keyExport = var_export((string)$key, true);
$lines[] = " {$keyExport} => [";
$list = is_array($value) ? $value : [];
foreach ($list as $c) {
$c = ltrim((string)$c, '\\');
if ($c === '') {
continue;
}
$lines[] = " {$c}::class,";
}
$lines[] = " ],";
}
$lines[] = "];\n";
return implode("\n", $lines);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,148 @@
<?php
namespace Webman\Console\Commands\Concerns;
use Webman\Console\Commands\Concerns\MakeCommandHelpers;
use Webman\Console\Messages;
use Webman\Console\Util;
trait PluginCommandHelpers
{
use MakeCommandHelpers;
/**
* Normalize plugin name input and canonicalize it to lowercase.
*
* @param mixed $value
* @return string|null
*/
protected function normalizePluginName(mixed $value): ?string
{
if ($value === null) {
return null;
}
$value = trim((string)$value);
return $value === '' ? null : strtolower($value);
}
/**
* Validate composer package name "vendor/name".
* We accept input and canonicalize to lowercase in normalizePluginName().
*
* @param string $name lowercase package name
* @return bool
*/
protected function isValidComposerPackageName(string $name): bool
{
if (substr_count($name, '/') !== 1) {
return false;
}
return (bool)preg_match('/^[a-z0-9](?:[a-z0-9_.-]*[a-z0-9])?\/[a-z0-9](?:[a-z0-9_.-]*[a-z0-9])?$/', $name);
}
/**
* Check whether a plugin package exists in current project.
*
* Rules:
* - Prefer directory existence under "<project>/vendor/<vendor>/<name>" (works for both composer-installed and local plugin skeletons).
* - Fallback to Composer runtime API when available.
*
* @param string $name composer package name in vendor/name format (lowercase recommended)
* @return bool
*/
protected function pluginPackageExists(string $name): bool
{
$relativeVendorPath = 'vendor' . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $name);
$vendorDir = base_path() . DIRECTORY_SEPARATOR . $relativeVendorPath;
if (is_dir($vendorDir)) {
return true;
}
if (class_exists(\Composer\InstalledVersions::class)) {
return \Composer\InstalledVersions::isInstalled($name);
}
return false;
}
/**
* Update "enable" flag in config/plugin/<name>/app.php with minimal diffs.
*
* @param string $configFile
* @param bool $enable
* @return array{ok:bool,changed:bool,already:bool,missingFile:bool,missingKey:bool,error:string|null}
*/
protected function setPluginEnableFlag(string $configFile, bool $enable): array
{
$result = [
'ok' => false,
'changed' => false,
'already' => false,
'missingFile' => false,
'missingKey' => false,
'error' => null,
];
if (!is_file($configFile)) {
$result['missingFile'] = true;
$result['ok'] = true;
return $result;
}
$config = $this->loadPhpConfigArray($configFile);
if ($config === null) {
$result['error'] = "Bad config file: {$configFile}";
return $result;
}
if (!array_key_exists('enable', $config)) {
$result['missingKey'] = true;
$result['ok'] = true;
return $result;
}
$current = (bool)$config['enable'];
if ($current === $enable) {
$result['already'] = true;
$result['ok'] = true;
return $result;
}
$content = file_get_contents($configFile);
if (!is_string($content) || $content === '') {
$result['error'] = "Unable to read file: {$configFile}";
return $result;
}
$target = $enable ? 'true' : 'false';
$pattern = '/([\'"]enable[\'"]\s*=>\s*)(true|false)/i';
$count = 0;
$patched = preg_replace($pattern, '$1' . $target, $content, -1, $count);
if (!is_string($patched) || $count === 0) {
// Do not rewrite full config to preserve user formatting/comments.
$result['error'] = "Config key 'enable' not found in file: {$configFile}";
return $result;
}
if (file_put_contents($configFile, $patched) === false) {
$result['error'] = "Unable to write file: {$configFile}";
return $result;
}
$result['ok'] = true;
$result['changed'] = true;
return $result;
}
/**
* CLI messages for plugin commands. Locale selected by getLocale() / Util fallback.
*
* @param string $key
* @param array $replace
* @return string
*/
protected function pluginMsg(string $key, array $replace = []): string
{
return strtr(Util::selectLocaleMessages(Messages::getPluginMessages())[$key] ?? $key, $replace);
}
}
@@ -0,0 +1,28 @@
<?php
namespace Webman\Console\Commands\Concerns;
use Symfony\Component\Console\Command\Command;
use Webman\Console\Application;
/**
* Shared execution logic for service management commands
* (start, stop, restart, reload, status, connections)
*/
trait ServiceCommandExecutor
{
/**
* Execute service command by delegating to appropriate application runner
*
* @return int
*/
protected function executeServiceCommand(): int
{
if (\class_exists(\Support\App::class)) {
\Support\App::run();
return Command::SUCCESS;
}
Application::run();
return Command::SUCCESS;
}
}
@@ -0,0 +1,33 @@
<?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 Webman\Console\Commands\Concerns\ServiceCommandExecutor;
use Webman\Console\Messages;
use Webman\Console\Util;
#[AsCommand('connections', 'Get worker connections')]
class ConnectionsCommand extends Command
{
use ServiceCommandExecutor;
protected function configure(): void
{
$messages = Util::selectLocaleMessages(Messages::getServiceMessages());
$this->setDescription($messages['connections_desc'] ?? 'Get worker connections');
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
return $this->executeServiceCommand();
}
}
@@ -0,0 +1,112 @@
<?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 Webman\Console\Commands\Concerns\MakeCommandHelpers;
use Webman\Console\Util;
use Webman\Console\Messages;
#[AsCommand('fix-disable-functions', 'Fix disbale_functions in php.ini')]
class FixDisbaleFunctionsCommand extends Command
{
use MakeCommandHelpers;
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$php_ini_file = php_ini_loaded_file();
if (!$php_ini_file) {
$output->writeln($this->msg('no_ini'));
return self::FAILURE;
}
$output->writeln($this->msg('location', ['{path}' => $php_ini_file]));
$disable_functions_str = ini_get("disable_functions");
if (!$disable_functions_str) {
$output->writeln($this->msg('ok'));
return self::SUCCESS;
}
$functions_required = [
"stream_socket_server",
"stream_socket_accept",
"stream_socket_client",
"pcntl_signal_dispatch",
"pcntl_signal",
"pcntl_alarm",
"pcntl_fork",
"posix_getuid",
"posix_getpwuid",
"posix_kill",
"posix_setsid",
"posix_getpid",
"posix_getpwnam",
"posix_getgrnam",
"posix_getgid",
"posix_setgid",
"posix_initgroups",
"posix_setuid",
"posix_isatty",
"proc_open",
"proc_get_status",
"proc_close",
"shell_exec",
"exec",
];
$disable_functions = explode(",", $disable_functions_str);
$disable_functions_removed = [];
foreach ($disable_functions as $index => $func) {
$func = trim($func);
foreach ($functions_required as $func_prefix) {
if (strpos($func, $func_prefix) === 0) {
$disable_functions_removed[$func] = $func;
unset($disable_functions[$index]);
}
}
}
$php_ini_content = file_get_contents($php_ini_file);
if (!is_string($php_ini_content) || $php_ini_content === '') {
$output->writeln($this->msg('ini_empty', ['{path}' => $php_ini_file]));
return self::FAILURE;
}
$disable_functions = array_values(array_filter(array_map('trim', $disable_functions), static fn($v) => $v !== ''));
$new_disable_functions_str = implode(",", $disable_functions);
// Replace existing line if present, otherwise append.
$pattern = '/(^|\\R)\\s*disable_functions\\s*=.*$/m';
if (preg_match($pattern, $php_ini_content)) {
$php_ini_content = preg_replace(
$pattern,
'$1disable_functions = ' . $new_disable_functions_str,
$php_ini_content,
1
);
} else {
$php_ini_content = rtrim($php_ini_content) . PHP_EOL . 'disable_functions = ' . $new_disable_functions_str . PHP_EOL;
}
file_put_contents($php_ini_file, $php_ini_content);
foreach ($disable_functions_removed as $func) {
$output->writeln($this->msg('enabled', ['{func}' => $func]));
}
$output->writeln($this->msg('success'));
return self::SUCCESS;
}
protected function msg(string $key, array $replace = []): string
{
return strtr(Util::selectLocaleMessages(Messages::getFixDisableFunctionsMessages())[$key] ?? $key, $replace);
}
}
+40
View File
@@ -0,0 +1,40 @@
<?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 Webman\Console\Commands\Concerns\MakeCommandHelpers;
use Webman\Console\Util;
use Webman\Console\Messages;
#[AsCommand('install', 'Execute webman installation script')]
class InstallCommand extends Command
{
use MakeCommandHelpers;
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln($this->msg('install_title'));
$install_function = "\\Webman\\Install::install";
if (is_callable($install_function)) {
$install_function();
$output->writeln($this->msg('done'));
return self::SUCCESS;
}
$output->writeln($this->msg('require_version'));
return self::FAILURE;
}
protected function msg(string $key, array $replace = []): string
{
return strtr(Util::selectLocaleMessages(Messages::getInstallMessages())[$key] ?? $key, $replace);
}
}
@@ -0,0 +1,270 @@
<?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\Messages;
use Webman\Console\Commands\Concerns\MakeCommandHelpers;
#[AsCommand('make:bootstrap', 'Make a bootstrap.')]
class MakeBootstrapCommand extends Command
{
use MakeCommandHelpers;
/**
* @return void
*/
protected function configure()
{
$this->addArgument('name', InputArgument::REQUIRED, $this->msg('arg_name'));
$this->addArgument('enable', InputArgument::OPTIONAL, $this->msg('arg_enable'));
$this->addOption('plugin', 'p', InputOption::VALUE_REQUIRED, $this->msg('opt_plugin'));
$this->addOption('path', 'P', InputOption::VALUE_REQUIRED, $this->msg('opt_path'));
$this->addOption('force', 'f', InputOption::VALUE_NONE, $this->msg('opt_force'));
$this->setHelp($this->buildHelpText());
$this->addUsage('MyBootstrap');
$this->addUsage('MyBootstrap no');
$this->addUsage('MyBootstrap -p admin');
$this->addUsage('MyBootstrap -P plugin/admin/app/bootstrap');
$this->addUsage('MyBootstrap -f');
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$name = Util::nameToClass((string)$input->getArgument('name'));
$enable = $this->parseEnableArgument($input->getArgument('enable'));
$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;
}
$name = str_replace('\\', '/', $name);
if (!$path && $input->isInteractive()) {
$pathDefault = $plugin ? Util::getDefaultAppRelativePath('bootstrap', $plugin) : Util::getDefaultAppRelativePath('bootstrap');
$path = $this->promptForPathWithDefault($input, $output, 'bootstrap', $pathDefault);
}
if ($plugin || $path) {
$resolved = $this->resolveTargetByPluginOrPath(
$name,
$plugin,
$path,
$output,
fn(string $p) => Util::getDefaultAppRelativePath('bootstrap', $p),
fn(string $key, array $replace = []) => $this->msg($key, $replace)
);
if ($resolved === null) {
return Command::FAILURE;
}
[$class, $namespace, $file] = $resolved;
} else {
[$class, $namespace, $file] = $this->resolveAppBootstrapTarget($name);
}
$output->writeln($this->msg('make_bootstrap', ['{name}' => $class]));
if (is_file($file) && !$force) {
$relative = $this->toRelativePath($file);
$prompt = $this->msg('override_prompt', ['{path}' => $relative]);
$question = new ConfirmationQuestion($prompt, true);
if (!$this->askOrAbort($input, $output, $question)) {
return Command::SUCCESS;
}
}
$this->createBootstrap($class, $namespace, $file);
$output->writeln($this->msg('created', ['{path}' => $this->toRelativePath($file)]));
if ($enable) {
$bootstrapClass = "{$namespace}\\{$class}";
$configFile = $plugin
? base_path('plugin' . DIRECTORY_SEPARATOR . $plugin . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'bootstrap.php')
: (config_path() . '/bootstrap.php');
$this->addClassToFlatClassListConfig($configFile, $bootstrapClass);
$output->writeln($this->msg('enabled', ['{class}' => $bootstrapClass]));
}
return self::SUCCESS;
}
/**
* Resolve bootstrap namespace/file path under app/ (backward compatible).
*
* @param string $name
* @return array{0:string,1:string,2:string} [class, namespace, file]
*/
protected function resolveAppBootstrapTarget(string $name): array
{
$bootstrapStr = Util::getDefaultAppPath('bootstrap');
$bootstrapRelPath = Util::getDefaultAppRelativePath('bootstrap');
$upper = strtolower($bootstrapStr[0]) !== $bootstrapStr[0];
if (!($pos = strrpos($name, '/'))) {
$class = ucfirst($name);
$file = app_path() . DIRECTORY_SEPARATOR . $bootstrapStr . DIRECTORY_SEPARATOR . "{$class}.php";
$namespace = Util::pathToNamespace($bootstrapRelPath);
return [$class, $namespace, $file];
}
$dirPart = substr($name, 0, $pos);
$realDirPart = Util::guessPath(app_path(), $dirPart);
if ($realDirPart) {
$dirPart = str_replace(DIRECTORY_SEPARATOR, '/', $realDirPart);
} else if ($upper) {
$dirPart = preg_replace_callback('/\/([a-z])/', static function ($matches) {
return '/' . strtoupper($matches[1]);
}, ucfirst($dirPart));
}
$appDirName = Util::detectAppDirName();
$path = "{$bootstrapStr}/{$dirPart}";
$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];
}
/**
* Default app bootstrap relative path.
*/
protected function getAppBootstrapRelativePath(): string
{
return Util::getDefaultAppRelativePath('bootstrap');
}
/**
* @param string $plugin
* @return string relative path
*/
protected function getPluginBootstrapRelativePath(string $plugin): string
{
return Util::getDefaultAppRelativePath('bootstrap', $plugin);
}
/**
* Prompt for path (question style, input on new line). Reuses enter_path_prompt from make:crud.
*/
protected function promptForPathWithDefault(InputInterface $input, OutputInterface $output, string $labelKey, string $defaultPath): string
{
$defaultPath = $this->normalizeRelativePath($defaultPath);
$label = Util::selectLocaleMessages(Messages::getTypeLabels())[$labelKey] ?? $labelKey;
$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);
}
/**
* @param $name
* @param $namespace
* @param $file
* @return void
*/
protected function createBootstrap($name, $namespace, $file)
{
$path = pathinfo($file, PATHINFO_DIRNAME);
if (!is_dir($path)) {
mkdir($path, 0777, true);
}
$bootstrap_content = <<<EOF
<?php
namespace $namespace;
use Webman\Bootstrap;
class $name implements Bootstrap
{
public static function start(\$worker)
{
// Is it console environment ?
\$is_console = !\$worker;
if (\$is_console) {
// If you do not want to execute this in console, just return.
return;
}
}
}
EOF;
file_put_contents($file, $bootstrap_content);
}
public function addConfig($class, $config_file)
{
$config = include $config_file;
if(!in_array($class, $config ?? [])) {
$config_file_content = file_get_contents($config_file);
$config_file_content = preg_replace('/\];/', " $class::class,\n];", $config_file_content);
file_put_contents($config_file, $config_file_content);
}
}
/**
* Parse positional `enable` argument (backward compatible).
*
* @param mixed $value
* @return bool
*/
protected function parseEnableArgument(mixed $value): bool
{
if ($value === null) {
return true;
}
$v = strtolower(trim((string)$value));
return !in_array($v, ['no', '0', 'false', 'n', 'off', 'disable', 'disabled'], true);
}
/**
* Command help text (multilingual).
*
* @return string
*/
protected function buildHelpText(): string
{
return Util::selectByLocale(Messages::getMakeBootstrapHelpText());
}
/**
* 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::getMakeBootstrapMessages())[$key] ?? $key, $replace);
}
}
@@ -0,0 +1,238 @@
<?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\Messages;
use Webman\Console\Commands\Concerns\MakeCommandHelpers;
#[AsCommand('make:command', 'Make command')]
class MakeCommandCommand extends Command
{
use MakeCommandHelpers;
/**
* @return void
*/
protected function configure()
{
$this->addArgument('name', InputArgument::REQUIRED, 'Command 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/command');
$this->addOption('force', 'f', InputOption::VALUE_NONE, 'Override existing file without confirmation.');
$this->setHelp($this->buildHelpText());
$this->addUsage('user:list');
$this->addUsage('user:list -p admin');
$this->addUsage('user:list -P plugin/admin/app/command');
$this->addUsage('user:list -f');
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$command = trim((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;
}
// make:command 不支持子目录(不允许 / 或 \)
$command = str_replace(['\\', '/'], '', $command);
if ($command === '') {
$output->writeln($this->msg('invalid_command'));
return Command::FAILURE;
}
$class = $this->commandToClassName($command);
if (!$path && $input->isInteractive()) {
$pathDefault = $plugin ? Util::getDefaultAppRelativePath('command', $plugin) : Util::getDefaultAppRelativePath('command');
$path = $this->promptForPathWithDefault($input, $output, 'command', $pathDefault);
}
if ($plugin || $path) {
$resolved = $this->resolveTargetByPluginOrPath(
$class,
$plugin,
$path,
$output,
fn(string $p) => Util::getDefaultAppRelativePath('command', $p),
fn(string $key, array $replace = []) => $this->msg($key, $replace)
);
if ($resolved === null) {
return Command::FAILURE;
}
[$class, $namespace, $file] = $resolved;
} else {
[$class, $namespace, $file] = $this->resolveAppCommandTarget($class);
}
$output->writeln($this->msg('make_command', ['{name}' => $command]));
if (is_file($file) && !$force) {
$relative = $this->toRelativePath($file);
$prompt = $this->msg('override_prompt', ['{path}' => $relative]);
$question = new ConfirmationQuestion($prompt, true);
if (!$this->askOrAbort($input, $output, $question)) {
return Command::SUCCESS;
}
}
$this->createCommand($class, $namespace, $file, $command);
$output->writeln($this->msg('created', ['{path}' => $this->toRelativePath($file)]));
return self::SUCCESS;
}
/**
* Convert a console command name (like "user:list") to a PHP class name (like "UserList").
*
* @param string $command
* @return string
*/
protected function commandToClassName(string $command): string
{
$items = array_values(array_filter(explode(':', $command), static fn($v) => $v !== ''));
$name = '';
foreach ($items as $item) {
// Support kebab/snake: foo-bar => FooBar
$tmp = Util::nameToClass(str_replace('-', '_', $item));
$tmp = str_replace(['\\', '/'], '', $tmp);
$name .= ucfirst($tmp);
}
return $name ?: 'ConsoleCommand';
}
/**
* Resolve command namespace/file path under app/ (backward compatible).
*
* @param string $class
* @return array{0:string,1:string,2:string} [class, namespace, file]
*/
protected function resolveAppCommandTarget(string $class): array
{
$commandStr = Util::getDefaultAppPath('command');
$commandRelPath = Util::getDefaultAppRelativePath('command');
$file = app_path() . DIRECTORY_SEPARATOR . $commandStr . DIRECTORY_SEPARATOR . "{$class}.php";
$namespace = Util::pathToNamespace($commandRelPath);
return [$class, $namespace, $file];
}
/**
* Default app command relative path.
*/
protected function getAppCommandRelativePath(): string
{
return Util::getDefaultAppRelativePath('command');
}
/**
* @param string $plugin
* @return string relative path
*/
protected function getPluginCommandRelativePath(string $plugin): string
{
return Util::getDefaultAppRelativePath('command', $plugin);
}
/**
* Prompt for path (question style, input on new line). Reuses enter_path_prompt from make:crud.
*/
protected function promptForPathWithDefault(InputInterface $input, OutputInterface $output, string $labelKey, string $defaultPath): string
{
$defaultPath = $this->normalizeRelativePath($defaultPath);
$label = Util::selectLocaleMessages(Messages::getTypeLabels())[$labelKey] ?? $labelKey;
$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);
}
/**
* @param $name
* @param $namespace
* @param $path
* @return void
*/
protected function createCommand($name, $namespace, $file, $command)
{
$path = pathinfo($file, PATHINFO_DIRNAME);
if (!is_dir($path)) {
mkdir($path, 0777, true);
}
$desc = str_replace(':', ' ', $command);
$command_content = <<<EOF
<?php
namespace $namespace;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand('$command', '$desc')]
class $name extends Command
{
protected function configure(): void
{
}
protected function execute(InputInterface \$input, OutputInterface \$output): int
{
\$output->writeln('<info>Hello</info> <comment>' . \$this->getName() . '</comment>');
return self::SUCCESS;
}
}
EOF;
file_put_contents($file, $command_content);
}
/**
* Command help text (multilingual).
*
* @return string
*/
protected function buildHelpText(): string
{
return Util::selectByLocale(Messages::getMakeCommandHelpText());
}
/**
* 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::getMakeCommandMessages())[$key] ?? $key, $replace);
}
}
@@ -0,0 +1,282 @@
<?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);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,235 @@
<?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\Messages;
use Webman\Console\Commands\Concerns\MakeCommandHelpers;
#[AsCommand('make:middleware', 'Make middleware')]
class MakeMiddlewareCommand extends Command
{
use MakeCommandHelpers;
/**
* @return void
*/
protected function configure()
{
$this->addArgument('name', InputArgument::REQUIRED, 'Middleware 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/middleware');
$this->addOption('force', 'f', InputOption::VALUE_NONE, 'Override existing file without confirmation.');
$this->setHelp($this->buildHelpText());
$this->addUsage('Auth');
$this->addUsage('Auth -p admin');
$this->addUsage('Auth -P plugin/admin/app/middleware');
$this->addUsage('Api/Auth -f');
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$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;
}
$name = str_replace('\\', '/', $name);
if (!$path && $input->isInteractive()) {
$pathDefault = $plugin ? Util::getDefaultAppRelativePath('middleware', $plugin) : Util::getDefaultAppRelativePath('middleware');
$path = $this->promptForPathWithDefault($input, $output, 'middleware', $pathDefault);
}
if ($plugin || $path) {
$resolved = $this->resolveTargetByPluginOrPath(
$name,
$plugin,
$path,
$output,
fn(string $p) => Util::getDefaultAppRelativePath('middleware', $p),
fn(string $key, array $replace = []) => $this->msg($key, $replace)
);
if ($resolved === null) {
return Command::FAILURE;
}
[$class, $namespace, $file] = $resolved;
} else {
[$class, $namespace, $file] = $this->resolveAppMiddlewareTarget($name);
}
$output->writeln($this->msg('make_middleware', ['{name}' => $class]));
if (is_file($file) && !$force) {
$relative = $this->toRelativePath($file);
$prompt = $this->msg('override_prompt', ['{path}' => $relative]);
$question = new ConfirmationQuestion($prompt, true);
if (!$this->askOrAbort($input, $output, $question)) {
return Command::SUCCESS;
}
}
$this->createMiddleware($class, $namespace, $file);
$output->writeln($this->msg('created', ['{path}' => $this->toRelativePath($file)]));
$middlewareClass = "{$namespace}\\{$class}";
$configFile = $plugin
? base_path('plugin' . DIRECTORY_SEPARATOR . $plugin . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'middleware.php')
: (config_path() . '/middleware.php');
$this->addClassToMiddlewareConfig($configFile, $middlewareClass);
$output->writeln($this->msg('configured', ['{class}' => $middlewareClass, '{file}' => $this->toRelativePath($configFile)]));
return self::SUCCESS;
}
/**
* Resolve middleware namespace/file path under app/ (backward compatible).
*
* @param string $name
* @return array{0:string,1:string,2:string} [class, namespace, file]
*/
protected function resolveAppMiddlewareTarget(string $name): array
{
$middlewareStr = Util::getDefaultAppPath('middleware');
$middlewareRelPath = Util::getDefaultAppRelativePath('middleware');
if (!($pos = strrpos($name, '/'))) {
$class = ucfirst($name);
$file = app_path() . DIRECTORY_SEPARATOR . $middlewareStr . DIRECTORY_SEPARATOR . "{$class}.php";
$namespace = Util::pathToNamespace($middlewareRelPath);
return [$class, $namespace, $file];
}
$upper = strtolower($middlewareStr[0]) !== $middlewareStr[0];
$dirPart = substr($name, 0, $pos);
$realDirPart = Util::guessPath(app_path(), $dirPart);
if ($realDirPart) {
$dirPart = str_replace(DIRECTORY_SEPARATOR, '/', $realDirPart);
} else if ($upper) {
$dirPart = preg_replace_callback('/\/([a-z])/', static function ($matches) {
return '/' . strtoupper($matches[1]);
}, ucfirst($dirPart));
}
$appDirName = Util::detectAppDirName();
$path = "{$middlewareStr}/{$dirPart}";
$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];
}
/**
* Default app middleware relative path.
*/
protected function getAppMiddlewareRelativePath(): string
{
return Util::getDefaultAppRelativePath('middleware');
}
/**
* @param string $plugin
* @return string relative path
*/
protected function getPluginMiddlewareRelativePath(string $plugin): string
{
return Util::getDefaultAppRelativePath('middleware', $plugin);
}
/**
* Prompt for path (question style, input on new line). Reuses enter_path_prompt from make:crud.
*/
protected function promptForPathWithDefault(InputInterface $input, OutputInterface $output, string $labelKey, string $defaultPath): string
{
$defaultPath = $this->normalizeRelativePath($defaultPath);
$label = Util::selectLocaleMessages(Messages::getTypeLabels())[$labelKey] ?? $labelKey;
$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);
}
/**
* @param $name
* @param $namespace
* @param $path
* @return void
*/
protected function createMiddleware($name, $namespace, $file)
{
$path = pathinfo($file, PATHINFO_DIRNAME);
if (!is_dir($path)) {
mkdir($path, 0777, true);
}
$middleware_content = <<<EOF
<?php
namespace $namespace;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
class $name implements MiddlewareInterface
{
public function process(Request \$request, callable \$handler) : Response
{
return \$handler(\$request);
}
}
EOF;
file_put_contents($file, $middleware_content);
}
/**
* 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::getMakeMiddlewareMessages())[$key] ?? $key, $replace);
}
/**
* Command help text (multilingual).
*
* @return string
*/
protected function buildHelpText(): string
{
return Util::selectByLocale(Messages::getMakeMiddlewareHelpText());
}
}
+785
View File
@@ -0,0 +1,785 @@
<?php
namespace Webman\Console\Commands;
use Doctrine\Inflector\InflectorFactory;
use support\Db;
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\OrmType;
use Webman\Console\Util;
use Webman\Console\Commands\Concerns\MakeCommandHelpers;
use Webman\Console\Commands\Concerns\OrmTableCommandHelpers;
use Webman\Console\Messages;
#[AsCommand('make:model', 'Make model')]
class MakeModelCommand extends Command
{
use MakeCommandHelpers;
use OrmTableCommandHelpers;
/**
* @return void
*/
protected function configure()
{
/**
* Usage quick notes:
* - Prefer explicit `--table/-t` when table name does not follow conventions.
* - Interactive table picker is available only when terminal supports input; press Enter for more, 0 for empty model, /keyword to filter.
*
* 常用提示:
* - 表名不符合约定时建议显式使用 `--table/-t`。
* - 交互式选表仅在支持输入的终端下启用:回车=更多,0=空模型,/关键字=过滤。
*/
$this->addArgument('name', InputArgument::OPTIONAL, 'Model name (optional in interactive mode)');
$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/model');
$this->addOption('table', 't', InputOption::VALUE_REQUIRED, 'Specify table name. e.g. wa_users');
$this->addOption('orm', 'o', InputOption::VALUE_REQUIRED, 'Select orm: laravel|thinkorm');
$this->addOption('database', 'd', InputOption::VALUE_OPTIONAL, 'Select database connection.');
$this->addOption('force', 'f', InputOption::VALUE_NONE, 'Override existing file without confirmation.');
// Symfony Console built-in help:
// - php webman make:model --help
// - php webman help make:model
$this->setHelp($this->buildHelpText());
// Display examples in synopsis (shown in --help).
$this->addUsage('');
$this->addUsage('User');
$this->addUsage('User -p admin');
$this->addUsage('User -P plugin/admin/app/model');
$this->addUsage('User -t wa_users -o laravel');
$this->addUsage('User -t=wa_users -o=thinkorm -f');
$this->addUsage('admin/User --table=wa_admin_users --orm=laravel --database=mysql');
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$nameArg = $this->normalizeOptionValue($input->getArgument('name'));
$plugin = $this->normalizeOptionValue($input->getOption('plugin'));
$pathOption = $this->normalizeOptionValue($input->getOption('path'));
$orm = $this->normalizeOptionValue($input->getOption('orm'));
$databaseOption = $this->normalizeOptionValue($input->getOption('database'));
$connection = $databaseOption;
$table = $this->normalizeOptionValue($input->getOption('table'));
$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;
}
$type = $this->resolveOrm($orm);
// Resolve & validate DB connection:
// - When --plugin/-p is provided and --database/-d is provided, use the plugin's connection config.
// e.g. "-p admin -d mysql" => "plugin.admin.mysql"
// - When no plugin is provided, validate that the given --database/-d exists.
// - When --database/-d is omitted, prefer plugin default connection when plugin has DB config.
[$ok, $connection] = $this->resolveAndValidateConnection($type, $plugin, $connection, $output);
if (!$ok) {
return Command::FAILURE;
}
// New interactive flow: when no model name is provided, go table -> name -> path.
if (!$nameArg) {
if (!$this->isTerminalInteractive($input)) {
$output->writeln($this->msg('name_required_non_interactive'));
return Command::FAILURE;
}
if (!$table) {
$table = $this->promptForTable($input, $output, $type, $connection, 'Model', $plugin, $databaseOption) ?: null;
}
$nameDefault = $table ? $this->suggestModelNameFromTable($table, $type, $connection, $plugin, $databaseOption) : 'Model';
$nameInput = $this->promptForModelNameWithDefault($input, $output, $nameDefault);
if (!$nameInput) {
$output->writeln($this->msg('invalid_name'));
return Command::FAILURE;
}
$nameNormalized = $this->normalizeClassLikeName($nameInput);
$defaultPath = $plugin ? Util::getDefaultAppRelativePath('model', $plugin) : Util::getDefaultAppRelativePath('model');
$pathFinal = $pathOption;
if (!$pathFinal) {
$pathFinal = $this->promptForModelPathWithDefault($input, $output, $defaultPath);
}
$pathFinal = $this->normalizeRelativePath($pathFinal ?: $defaultPath);
// Resolve namespace/file by --plugin/--path rules:
// - If --path is explicitly provided, keep the strict plugin/path conflict check.
// - If --path is not provided, allow user to override the default path even when --plugin is set.
if ($plugin && $pathOption) {
$resolved = $this->resolveModelTargetByPluginOrPath($nameNormalized, $plugin, $pathOption, $output);
} else if ($plugin && !$pathOption) {
$expected = $this->getPluginModelRelativePath($plugin);
$resolved = $this->pathsEqual($expected, $pathFinal)
? $this->resolveModelTargetByPluginOrPath($nameNormalized, $plugin, null, $output)
: $this->resolveModelTargetByPluginOrPath($nameNormalized, null, $pathFinal, $output);
} else {
$resolved = $this->resolveModelTargetByPluginOrPath($nameNormalized, null, $pathFinal, $output);
}
if ($resolved === null) {
return Command::FAILURE;
}
[$name, $namespace, $file] = $resolved;
} else {
// Backward compatible behavior when model name is explicitly provided.
$name = Util::nameToClass($nameArg);
// Table selection before path selection.
if ($nameArg && !$table) {
$table = $this->promptForTableIfNeeded($input, $output, $type, $connection, $name, $plugin, $databaseOption) ?: null;
}
// When path is not provided and interactive: prompt for path after table.
if (!$pathOption && $input->isInteractive()) {
$pathDefault = $plugin ? Util::getDefaultAppRelativePath('model', $plugin) : Util::getDefaultAppRelativePath('model');
$pathOption = $this->promptForModelPathWithDefault($input, $output, $pathDefault);
}
if ($plugin || $pathOption) {
$resolved = $this->resolveModelTargetByPluginOrPath($name, $plugin, $pathOption, $output);
if ($resolved === null) {
return Command::FAILURE;
}
[$name, $namespace, $file] = $resolved;
} else {
// Original behavior for app models (backward compatible)
if (!($pos = strrpos($name, '/'))) {
$name = ucfirst($name);
$model_str = Util::getDefaultAppPath('model');
$file = app_path() . DIRECTORY_SEPARATOR . $model_str . DIRECTORY_SEPARATOR . "$name.php";
$namespace = Util::pathToNamespace(Util::getDefaultAppRelativePath('model'));
} else {
$name_str = substr($name, 0, $pos);
if ($real_name_str = Util::guessPath(app_path(), $name_str)) {
$name_str = $real_name_str;
} else if ($real_section_name = Util::guessPath(app_path(), strstr($name_str, '/', true))) {
$upper = strtolower($real_section_name[0]) !== $real_section_name[0];
} else {
$model_str_check = Util::getDefaultAppPath('model');
$upper = strtolower($model_str_check[0]) !== $model_str_check[0];
}
$upper = $upper ?? strtolower($name_str[0]) !== $name_str[0];
if ($upper && !$real_name_str) {
$name_str = preg_replace_callback('/\/([a-z])/', function ($matches) {
return '/' . strtoupper($matches[1]);
}, ucfirst($name_str));
}
$appDirName = Util::detectAppDirName();
$model_dir = Util::getDefaultAppPath('model');
$path_for_file = "$name_str/" . $model_dir;
$name = ucfirst(substr($name, $pos + 1));
$file = app_path() . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $path_for_file) . DIRECTORY_SEPARATOR . "$name.php";
$namespace = str_replace('/', '\\', $appDirName . '/' . $path_for_file);
}
}
}
$output->writeln($this->msg('make_model', ['{name}' => $name]));
if (is_file($file) && !$force) {
$relative = $this->toRelativePath($file);
$prompt = $this->msg('override_prompt', ['{path}' => $relative]);
$question = new ConfirmationQuestion($prompt, true);
if (!$this->askOrAbort($input, $output, $question)) {
return Command::SUCCESS;
}
}
if ($type === OrmType::THINKORM) {
$this->createTpModel($name, $namespace, $file, $connection, $table, $output);
} else {
$this->createModel($name, $namespace, $file, $connection, $table, $output);
}
$output->writeln($this->msg('created', ['{path}' => $this->toRelativePath($file)]));
return self::SUCCESS;
}
/**
* Prompt for model name with a default value.
*/
protected function promptForModelNameWithDefault(InputInterface $input, OutputInterface $output, string $default): string
{
$default = $this->normalizeClassLikeName($default ?: 'Model');
$question = new Question($this->msg('enter_model_name_prompt', ['{default}' => $default]), $default);
$answer = $this->askOrAbort($input, $output, $question);
$answer = is_string($answer) ? trim($answer) : '';
$answer = $answer !== '' ? $answer : $default;
return $this->normalizeClassLikeName($answer);
}
/**
* Prompt for model path with a default value.
*/
protected function promptForModelPathWithDefault(InputInterface $input, OutputInterface $output, string $default): string
{
$default = $this->normalizeRelativePath($default ?: 'app/model');
$question = new Question($this->msg('enter_model_path_prompt', ['{default}' => $default]), $default);
$answer = $this->askOrAbort($input, $output, $question);
$answer = is_string($answer) ? trim($answer) : '';
$answer = $answer !== '' ? $answer : $default;
return $this->normalizeRelativePath($answer ?: $default);
}
/**
* Normalize "class-like" names with optional sub paths, e.g.:
* - "admin/user_profile" => "admin/UserProfile"
* - "Admin\\User" => "admin/User"
*/
protected function normalizeClassLikeName(string $name): string
{
$name = trim($name);
if ($name === '') {
return '';
}
$name = str_replace('\\', '/', $name);
$name = trim($name, '/');
$segments = array_values(array_filter(explode('/', $name), static fn(string $s): bool => $s !== ''));
$segments = array_map(static fn(string $seg): string => Util::nameToClass($seg), $segments);
return implode('/', $segments);
}
/**
* Default app model relative path. Prefer existing "Model/model" directory.
*/
protected function getAppModelRelativePath(): string
{
return Util::getDefaultAppRelativePath('model');
}
/**
* @param $class
* @param $namespace
* @param $file
* @param string|null $connection
* @param string|null $table
* @param OutputInterface|null $output
* @return void
*/
protected function createModel($class, $namespace, $file, $connection = null, $table = null, ?OutputInterface $output = null)
{
$path = pathinfo($file, PATHINFO_DIRNAME);
if (!is_dir($path)) {
mkdir($path, 0777, true);
}
$table_base = Util::classToName($class);
$table_val = 'null';
$meta_table = null;
$pk = 'id';
$properties = '';
$connection = $connection ?: config('database.default');
$timestamps = 'false';
$hasCreatedAt = false;
$hasUpdatedAt = false;
$dbFailed = false;
$dbFailed = false;
try {
$connectionConfig = $this->getLaravelConnectionConfig((string)$connection);
$prefix = (string)($connectionConfig['prefix'] ?? '');
$database = (string)($connectionConfig['database'] ?? '');
$driver = (string)($connectionConfig['driver'] ?? 'mysql');
$inflector = InflectorFactory::create()->build();
$table_plura = $inflector->pluralize($inflector->tableize($class));
$con = Db::connection($connection);
// Table resolve
if ($table) {
$table = ltrim(trim((string)$table), '=');
$table_for_model = ($prefix && str_starts_with($table, $prefix)) ? substr($table, strlen($prefix)) : $table;
$table_val = "'" . $table_for_model . "'";
$meta_table = $prefix . $table_for_model;
} else {
// 检查表是否存在(兼容MySQL和PostgreSQL
if ($driver === 'pgsql') {
$schema = (string)($connectionConfig['schema'] ?? 'public');
$exists_plura = $con->select("SELECT to_regclass('{$schema}.{$prefix}{$table_plura}') as table_exists");
$exists = $con->select("SELECT to_regclass('{$schema}.{$prefix}{$table_base}') as table_exists");
if (!empty($exists_plura[0]->table_exists)) {
$table_val = "'$table_plura'";
$meta_table = "{$prefix}{$table_plura}";
} else if (!empty($exists[0]->table_exists)) {
$table_val = "'$table_base'";
$meta_table = "{$prefix}{$table_base}";
}
} else {
if ($con->select("show tables like '{$prefix}{$table_plura}'")) {
$table_val = "'$table_plura'";
$meta_table = "{$prefix}{$table_plura}";
} else if ($con->select("show tables like '{$prefix}{$table_base}'")) {
$table_val = "'$table_base'";
$meta_table = "{$prefix}{$table_base}";
}
}
}
// 获取表注释和列信息(兼容MySQL和PostgreSQL
if ($meta_table) {
if ($driver === 'pgsql') {
$schema = (string)($connectionConfig['schema'] ?? 'public');
$tableComment = $con->select("SELECT obj_description('{$schema}.{$meta_table}'::regclass) as table_comment");
if (!empty($tableComment) && !empty($tableComment[0]->table_comment)) {
$comments = $tableComment[0]->table_comment;
$properties .= " * {$meta_table} {$comments}" . PHP_EOL;
}
$columns = $con->select("
SELECT
a.attname as column_name,
format_type(a.atttypid, a.atttypmod) as data_type,
CASE WHEN con.contype = 'p' THEN 'PRI' ELSE '' END as column_key,
d.description as column_comment
FROM pg_catalog.pg_attribute a
LEFT JOIN pg_catalog.pg_description d ON d.objoid = a.attrelid AND d.objsubid = a.attnum
LEFT JOIN pg_catalog.pg_constraint con ON con.conrelid = a.attrelid AND a.attnum = ANY(con.conkey) AND con.contype = 'p'
WHERE a.attrelid = '{$schema}.{$meta_table}'::regclass
AND a.attnum > 0 AND NOT a.attisdropped
ORDER BY a.attnum
");
foreach ($columns as $item) {
if ($item->column_key === 'PRI') {
$pk = $item->column_name;
$item->column_comment = ($item->column_comment ? $item->column_comment . ' ' : '') . "(主键)";
}
$type = $this->getType($item->data_type);
if ($item->column_name === 'created_at') {
$hasCreatedAt = true;
}
if ($item->column_name === 'updated_at') {
$hasUpdatedAt = true;
}
$properties .= " * @property $type \${$item->column_name} " . ($item->column_comment ?? '') . "\n";
}
} else {
$tableComment = $con->select('SELECT table_comment FROM information_schema.`TABLES` WHERE table_schema = ? AND table_name = ?', [$database, $meta_table]);
if (!empty($tableComment)) {
$comments = $tableComment[0]->table_comment ?? $tableComment[0]->TABLE_COMMENT;
$properties .= " * {$meta_table} {$comments}" . PHP_EOL;
}
foreach ($con->select("select COLUMN_NAME,DATA_TYPE,COLUMN_KEY,COLUMN_COMMENT from INFORMATION_SCHEMA.COLUMNS where table_name = '$meta_table' and table_schema = '$database' ORDER BY ordinal_position") as $item) {
if ($item->COLUMN_KEY === 'PRI') {
$pk = $item->COLUMN_NAME;
$item->COLUMN_COMMENT .= "(主键)";
}
$type = $this->getType($item->DATA_TYPE);
if ($item->COLUMN_NAME === 'created_at') {
$hasCreatedAt = true;
}
if ($item->COLUMN_NAME === 'updated_at') {
$hasUpdatedAt = true;
}
$properties .= " * @property $type \${$item->COLUMN_NAME} {$item->COLUMN_COMMENT}\n";
}
}
} else if ($table) {
$output?->writeln($this->msg('table_not_found_schema', ['{table}' => (string)$table]));
}
} catch (\Throwable $e) {
$dbFailed = true;
$this->reportException($e, $output);
}
if (!$table && !$meta_table) {
$output?->writeln($dbFailed ? $this->msg('table_not_found_empty_db_failed') : $this->msg('table_not_found_empty'));
}
$properties = rtrim($properties) ?: ' *';
$timestamps = $hasCreatedAt && $hasUpdatedAt ? 'true' : 'false';
$model_content = <<<EOF
<?php
namespace $namespace;
use support\Model;
/**
$properties
*/
class $class extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected \$connection = '$connection';
/**
* The table associated with the model.
*
* @var string
*/
protected \$table = $table_val;
/**
* The primary key associated with the table.
*
* @var string
*/
protected \$primaryKey = '$pk';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public \$timestamps = $timestamps;
}
EOF;
file_put_contents($file, $model_content);
}
/**
* @param $class
* @param $namespace
* @param $file
* @param string|null $connection
* @param string|null $table
* @param OutputInterface|null $output
* @return void
*/
protected function createTpModel($class, $namespace, $file, $connection = null, $table = null, ?OutputInterface $output = null)
{
$path = pathinfo($file, PATHINFO_DIRNAME);
if (!is_dir($path)) {
mkdir($path, 0777, true);
}
$table_base = Util::classToName($class);
$is_thinkorm_v2 = class_exists(\support\think\Db::class);
$table_val = 'null';
$meta_table = null;
$pk = 'id';
$properties = '';
try {
$config_name = $is_thinkorm_v2 ? 'think-orm' : 'thinkorm';
$connection = $connection ?: (config("$config_name.default") ?: 'mysql');
$connectionConfig = $this->getThinkOrmConnectionConfig($config_name, (string)$connection);
$prefix = (string)($connectionConfig['prefix'] ?? '');
$database = (string)($connectionConfig['database'] ?? '');
$driver = (string)($connectionConfig['type'] ?? 'mysql');
$inflector = InflectorFactory::create()->build();
$table_plural = $inflector->pluralize($inflector->tableize($class));
if ($is_thinkorm_v2) {
$con = \support\think\Db::connect($connection);
} else {
$con = \think\facade\Db::connect($connection);
}
// Table resolve (ThinkORM Model `$table` will be treated as an exact table name and will NOT apply prefix automatically)
if ($table) {
$table = ltrim(trim((string)$table), '=');
if ($prefix && !str_starts_with($table, $prefix)) {
$table = $prefix . $table;
}
$table_val = "'" . $table . "'";
$meta_table = $table;
} else {
// 检查表是否存在(兼容MySQL和PostgreSQL
if ($driver === 'pgsql') {
$schema = (string)($connectionConfig['schema'] ?? 'public');
$exists_plural = $con->query("SELECT to_regclass('{$schema}.{$prefix}{$table_plural}') as table_exists");
$exists = $con->query("SELECT to_regclass('{$schema}.{$prefix}{$table_base}') as table_exists");
if (!empty($exists_plural[0]['table_exists'])) {
$meta_table = "{$prefix}{$table_plural}";
$table_val = "'" . $meta_table . "'";
} else if (!empty($exists[0]['table_exists'])) {
$meta_table = "{$prefix}{$table_base}";
$table_val = "'" . $meta_table . "'";
}
} else {
if ($con->query("show tables like '{$prefix}{$table_plural}'")) {
$meta_table = "{$prefix}{$table_plural}";
$table_val = "'" . $meta_table . "'";
} else if ($con->query("show tables like '{$prefix}{$table_base}'")) {
$meta_table = "{$prefix}{$table_base}";
$table_val = "'" . $meta_table . "'";
}
}
}
// 获取表注释和列信息(兼容MySQL和PostgreSQL
if ($meta_table) {
if ($driver === 'pgsql') {
$schema = (string)($connectionConfig['schema'] ?? 'public');
$tableComment = $con->query("SELECT obj_description('{$schema}.{$meta_table}'::regclass) as table_comment");
if (!empty($tableComment) && !empty($tableComment[0]['table_comment'])) {
$comments = $tableComment[0]['table_comment'];
$properties .= " * {$meta_table} {$comments}" . PHP_EOL;
}
$columns = $con->query("
SELECT
a.attname as column_name,
format_type(a.atttypid, a.atttypmod) as data_type,
CASE WHEN con.contype = 'p' THEN 'PRI' ELSE '' END as column_key,
d.description as column_comment
FROM pg_catalog.pg_attribute a
LEFT JOIN pg_catalog.pg_description d ON d.objoid = a.attrelid AND d.objsubid = a.attnum
LEFT JOIN pg_catalog.pg_constraint con ON con.conrelid = a.attrelid AND a.attnum = ANY(con.conkey) AND con.contype = 'p'
WHERE a.attrelid = '{$schema}.{$meta_table}'::regclass
AND a.attnum > 0 AND NOT a.attisdropped
ORDER BY a.attnum
");
foreach ($columns as $item) {
if ($item['column_key'] === 'PRI') {
$pk = $item['column_name'];
$item['column_comment'] = ($item['column_comment'] ? $item['column_comment'] . ' ' : '') . "(主键)";
}
$type = $this->getType($item['data_type']);
$properties .= " * @property $type \${$item['column_name']} " . ($item['column_comment'] ?? '') . "\n";
}
} else {
$tableComment = $con->query('SELECT table_comment FROM information_schema.`TABLES` WHERE table_schema = ? AND table_name = ?', [$database, $meta_table]);
if (!empty($tableComment)) {
$comments = $tableComment[0]['table_comment'] ?? $tableComment[0]['TABLE_COMMENT'];
$properties .= " * {$meta_table} {$comments}" . PHP_EOL;
}
foreach ($con->query("select COLUMN_NAME,DATA_TYPE,COLUMN_KEY,COLUMN_COMMENT from INFORMATION_SCHEMA.COLUMNS where table_name = '$meta_table' and table_schema = '$database' ORDER BY ordinal_position") as $item) {
if ($item['COLUMN_KEY'] === 'PRI') {
$pk = $item['COLUMN_NAME'];
$item['COLUMN_COMMENT'] .= "(主键)";
}
$type = $this->getType($item['DATA_TYPE']);
$properties .= " * @property $type \${$item['COLUMN_NAME']} {$item['COLUMN_COMMENT']}\n";
}
}
} else if ($table) {
$output?->writeln($this->msg('table_not_found_schema', ['{table}' => (string)$table]));
}
} catch (\Throwable $e) {
$dbFailed = true;
$this->reportException($e, $output);
}
if (!$table && !$meta_table) {
$output?->writeln($dbFailed ? $this->msg('table_not_found_empty_db_failed') : $this->msg('table_not_found_empty'));
}
$properties = rtrim($properties) ?: ' *';
$modelNamespace = $is_thinkorm_v2 ? 'support\think\Model' : 'think\Model';
$model_content = <<<EOF
<?php
namespace $namespace;
use $modelNamespace;
/**
$properties
*/
class $class extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected \$connection = '$connection';
/**
* The table associated with the model.
*
* @var string
*/
protected \$table = $table_val;
/**
* The primary key associated with the table.
*
* @var string
*/
protected \$pk = '$pk';
}
EOF;
file_put_contents($file, $model_content);
}
/**
* @param \Throwable $e
* @param OutputInterface|null $output
* @return void
*/
protected function reportException(\Throwable $e, ?OutputInterface $output = null): void
{
if ($output) {
$this->writeDbNativeErrorOnce($output, $e, 'comment');
return;
}
echo $e->getMessage() . PHP_EOL;
}
/**
* Resolve model namespace/file path by --plugin/-p or --path/-P.
* - --plugin/-p: generate under plugin/<plugin>/app/model by default.
* - --path/-P: generate under the specified directory (relative to base path).
* - If both are provided, they must point to the same directory, otherwise it's an error.
*
* @param string $name
* @param string|null $plugin
* @param string|null $path
* @param OutputInterface $output
* @return array{0:string,1:string,2:string}|null [class, namespace, file]
*/
protected function resolveModelTargetByPluginOrPath(string $name, ?string $plugin, ?string $path, OutputInterface $output): ?array
{
$pathNorm = $path ? $this->normalizeRelativePath($path) : null;
if ($pathNorm !== null && $this->isAbsolutePath($pathNorm)) {
$output->writeln($this->msg('invalid_path', ['{path}' => $path]));
return null;
}
// Validate plugin existence (from --plugin/-p or inferred from --path/-P).
$pluginToCheck = $this->normalizeOptionValue($plugin) ?: $this->extractPluginNameFromRelativePath($pathNorm);
if ($pluginToCheck && !$this->assertPluginExists($pluginToCheck, $output)) {
return null;
}
$expected = null;
if ($plugin) {
$expected = $this->getPluginModelRelativePath($plugin);
}
if ($plugin && $pathNorm) {
$pluginPrefix = 'plugin/' . $plugin . '/';
if (!str_starts_with(strtolower($pathNorm), strtolower($pluginPrefix))) {
$output->writeln(strtr(
Util::selectByLocale(Messages::getPluginPathConflictMessage()),
['{plugin}' => $plugin, '{path}' => $pathNorm]
));
return null;
}
}
$targetRel = $pathNorm ?: $expected;
if (!$targetRel) {
return null;
}
$targetDir = base_path($targetRel);
$namespaceRoot = trim(str_replace('/', '\\', $targetRel), '\\');
if (!($pos = strrpos($name, '/'))) {
$class = ucfirst($name);
$subPath = '';
} else {
$subPath = substr($name, 0, $pos);
$class = ucfirst(substr($name, $pos + 1));
}
$subDir = $subPath ? str_replace('/', DIRECTORY_SEPARATOR, $subPath) . DIRECTORY_SEPARATOR : '';
$file = $targetDir . DIRECTORY_SEPARATOR . $subDir . $class . '.php';
$namespace = $namespaceRoot . ($subPath ? '\\' . str_replace('/', '\\', $subPath) : '');
return [$class, $namespace, $file];
}
/**
* @param string $plugin
* @return string relative path
*/
protected function getPluginModelRelativePath(string $plugin): string
{
return Util::getDefaultAppRelativePath('model', $plugin);
}
/**
* Hardcoded CLI messages without translation module.
*
* @param string $key
* @param array $replace
* @return string
*/
protected function msg(string $key, array $replace = []): string
{
return strtr(Util::selectLocaleMessages(Messages::getMakeModelMessages())[$key] ?? $key, $replace);
}
/**
* Command help text (multilingual).
*
* @return string
*/
protected function buildHelpText(): string
{
return Util::selectByLocale(Messages::getMakeModelHelpText());
}
/**
* @param string $type
* @return string
*/
protected function getType(string $type)
{
if (strpos($type, 'int') !== false) {
return 'integer';
}
if (strpos($type, 'character varying') !== false || strpos($type, 'varchar') !== false) {
return 'string';
}
if (strpos($type, 'timestamp') !== false) {
return 'string';
}
switch ($type) {
case 'varchar':
case 'string':
case 'text':
case 'date':
case 'time':
case 'guid':
case 'datetimetz':
case 'datetime':
case 'decimal':
case 'enum':
case 'character': // PostgreSQL类型
case 'char': // PostgreSQL类型
case 'json': // PostgreSQL类型
case 'jsonb': // PostgreSQL类型
case 'uuid': // PostgreSQL类型
case 'timestamptz': // PostgreSQL类型
case 'citext': // PostgreSQL类型
return 'string';
case 'boolean':
case 'bool': // PostgreSQL类型
return 'bool';
case 'float':
case 'float4': // PostgreSQL类型 (real)
case 'float8': // PostgreSQL类型 (double precision)
return 'float';
case 'numeric': // PostgreSQL类型
return 'string';
default:
return 'mixed';
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,242 @@
<?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\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Webman\Console\Util;
use Webman\Console\Commands\Concerns\PluginCommandHelpers;
#[AsCommand('plugin:create', 'Create plugin')]
class PluginCreateCommand extends Command
{
use PluginCommandHelpers;
/**
* @return void
*/
protected function configure()
{
// Do NOT use "-n": Symfony Console already reserves "-n" for "--no-interaction".
$this->addArgument('name', InputArgument::OPTIONAL, $this->pluginMsg('description_name'));
$this->addOption('name', null, InputOption::VALUE_REQUIRED, $this->pluginMsg('description_name'));
$this->setHelp($this->buildHelpText());
$this->addUsage('foo/my-admin');
$this->addUsage('--name foo/my-admin');
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$nameArg = $this->normalizePluginName($input->getArgument('name'));
$nameOpt = $this->normalizePluginName($input->getOption('name'));
if ($nameArg && $nameOpt && $nameArg !== $nameOpt) {
$output->writeln($this->pluginMsg('name_conflict', ['{arg}' => $nameArg, '{opt}' => $nameOpt]));
return Command::FAILURE;
}
$nameRaw = $nameOpt ?: $nameArg;
if (!$nameRaw) {
$output->writeln($this->pluginMsg('missing_name'));
return Command::FAILURE;
}
if (!$this->isValidComposerPackageName($nameRaw)) {
$output->writeln($this->pluginMsg('bad_name', ['{name}' => (string)$nameRaw]));
return Command::FAILURE;
}
$output->writeln($this->pluginMsg('create_title', ['{name}' => $nameRaw]));
$namespace = Util::nameToNamespace($nameRaw);
// Create dir config/plugin/$name
if (is_dir($plugin_config_path = config_path()."/plugin/$nameRaw")) {
$output->writeln($this->pluginMsg('dir_exists', ['{path}' => $this->toRelativePath($plugin_config_path)]));
return Command::FAILURE;
}
if (is_dir($plugin_path = base_path()."/vendor/$nameRaw")) {
$output->writeln($this->pluginMsg('dir_exists', ['{path}' => $this->toRelativePath($plugin_path)]));
return Command::FAILURE;
}
// Add psr-4
$output->writeln($this->pluginMsg('step_psr4', [
'{key}' => rtrim($namespace, '\\') . '\\',
'{path}' => "vendor/{$nameRaw}/src",
]));
if ($err = $this->addAutoloadToComposerJson($nameRaw, $namespace)) {
$output->writeln($this->pluginMsg('psr4_failed', ['{error}' => $err]));
return Command::FAILURE;
}
$output->writeln($this->pluginMsg('psr4_ok'));
$output->writeln($this->pluginMsg('step_config', ['{path}' => $this->toRelativePath($plugin_config_path)]));
if ($err = $this->createConfigFiles($plugin_config_path)) {
$output->writeln($this->pluginMsg('create_failed', ['{error}' => $err]));
return Command::FAILURE;
}
$output->writeln($this->pluginMsg('created', ['{path}' => $this->toRelativePath($plugin_config_path . '/app.php')]));
$output->writeln($this->pluginMsg('step_vendor', ['{path}' => $this->toRelativePath($plugin_path)]));
if ($err = $this->createVendorFiles($nameRaw, $namespace, $plugin_path, $output)) {
$output->writeln($this->pluginMsg('create_failed', ['{error}' => $err]));
return Command::FAILURE;
}
$output->writeln($this->pluginMsg('done', ['{name}' => $nameRaw]));
return Command::SUCCESS;
}
/**
* @param string $name
* @param string $namespace
* @return string|null error message
*/
protected function addAutoloadToComposerJson(string $name, string $namespace): ?string
{
if (!is_file($composer_json_file = base_path()."/composer.json")) {
return "$composer_json_file not exists";
}
$composer_json_str = file_get_contents($composer_json_file);
if (!is_string($composer_json_str) || $composer_json_str === '') {
return "Bad $composer_json_file";
}
$composer_json = json_decode($composer_json_str, true);
if (!$composer_json) {
return "Bad $composer_json_file";
}
$psr4Key = rtrim($namespace, '\\') . "\\";
$psr4Path = "vendor/$name/src";
if (isset($composer_json['autoload']['psr-4'][$psr4Key])) {
return null;
}
// Prefer surgical insertion to avoid rewriting whole composer.json format.
$line = json_encode($psr4Key, JSON_UNESCAPED_SLASHES) . ': ' . json_encode($psr4Path, JSON_UNESCAPED_SLASHES) . ",\n";
$pattern = '/("psr-4"\s*:\s*\{\s*\n)(\s*)"/';
if (preg_match($pattern, $composer_json_str)) {
$patched = preg_replace_callback($pattern, static function ($m) use ($line) {
return $m[1] . $m[2] . $line . $m[2] . '"';
}, $composer_json_str, 1);
if (is_string($patched) && json_decode($patched, true)) {
file_put_contents($composer_json_file, $patched);
return null;
}
}
// Fallback: rewrite composer.json (stable, but may change formatting).
if (!isset($composer_json['autoload']) || !is_array($composer_json['autoload'])) {
$composer_json['autoload'] = [];
}
if (!isset($composer_json['autoload']['psr-4']) || !is_array($composer_json['autoload']['psr-4'])) {
$composer_json['autoload']['psr-4'] = [];
}
$composer_json['autoload']['psr-4'] = [$psr4Key => $psr4Path] + $composer_json['autoload']['psr-4'];
$encoded = json_encode($composer_json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
if (!is_string($encoded) || $encoded === '') {
return "Bad $composer_json_file";
}
file_put_contents($composer_json_file, $encoded . "\n");
return null;
}
/**
* @param string $plugin_config_path
* @return string|null error message
*/
protected function createConfigFiles(string $plugin_config_path): ?string
{
if (!mkdir($plugin_config_path, 0777, true) && !is_dir($plugin_config_path)) {
return "Unable to create directory {$plugin_config_path}";
}
$app_str = <<<EOF
<?php
return [
'enable' => true,
];
EOF;
$ret = file_put_contents("$plugin_config_path/app.php", $app_str);
if ($ret === false) {
return "Unable to write file {$plugin_config_path}/app.php";
}
return null;
}
/**
* @param string $name
* @param string $namespace
* @param string $plugin_path
* @param OutputInterface $output
* @return string|null error message
*/
protected function createVendorFiles(string $name, string $namespace, string $plugin_path, OutputInterface $output): ?string
{
if (!mkdir("$plugin_path/src", 0777, true) && !is_dir("$plugin_path/src")) {
return "Unable to create directory {$plugin_path}/src";
}
if (!$this->createComposerJson($name, $namespace, $plugin_path)) {
return "Unable to write file {$plugin_path}/composer.json";
}
$output->writeln($this->pluginMsg('created', ['{path}' => $this->toRelativePath($plugin_path . '/composer.json')]));
$baseCmd = "composer dumpautoload";
$result = Util::exec("$baseCmd --no-interaction");
if ($result === null) {
$output->writeln($this->pluginMsg('dumpautoload_manual', ['{cmd}' => $baseCmd]));
return null;
}
if ($result['exit_code'] !== 0) {
$output->writeln($this->pluginMsg('dumpautoload_failed', ['{cmd}' => $baseCmd]));
return null;
}
$output->writeln($this->pluginMsg('dumpautoload_ok'));
return null;
}
/**
* @param string $name
* @param string $namespace
* @param string $dest
* @return bool
*/
protected function createComposerJson(string $name, string $namespace, string $dest): bool
{
$namespace = str_replace('\\', '\\\\', $namespace);
$composer_json_content = <<<EOT
{
"name": "$name",
"type": "library",
"license": "MIT",
"description": "Webman plugin $name",
"require": {
},
"autoload": {
"psr-4": {
"$namespace\\\\": "src"
}
}
}
EOT;
return file_put_contents("$dest/composer.json", $composer_json_content) !== false;
}
/**
* Command help text (bilingual).
*
* @return string
*/
protected function buildHelpText(): string
{
return Util::selectByLocale(\Webman\Console\Messages::getPluginCreateHelpText());
}
}
@@ -0,0 +1,91 @@
<?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\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Webman\Console\Commands\Concerns\PluginCommandHelpers;
use Webman\Console\Util;
#[AsCommand('plugin:disable', 'Disable plugin by name')]
class PluginDisableCommand extends Command
{
use PluginCommandHelpers;
/**
* @return void
*/
protected function configure()
{
// Do NOT use "-n": Symfony Console already reserves "-n" for "--no-interaction".
$this->addArgument('name', InputArgument::OPTIONAL, $this->pluginMsg('description_name'));
$this->addOption('name', null, InputOption::VALUE_REQUIRED, $this->pluginMsg('description_name'));
$this->setHelp($this->buildHelpText());
$this->addUsage('foo/my-admin');
$this->addUsage('--name foo/my-admin');
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$nameArg = $this->normalizePluginName($input->getArgument('name'));
$nameOpt = $this->normalizePluginName($input->getOption('name'));
if ($nameArg && $nameOpt && $nameArg !== $nameOpt) {
$output->writeln($this->pluginMsg('name_conflict', ['{arg}' => $nameArg, '{opt}' => $nameOpt]));
return Command::FAILURE;
}
$nameRaw = $nameOpt ?: $nameArg;
if (!$nameRaw) {
$output->writeln($this->pluginMsg('missing_name'));
return Command::FAILURE;
}
if (!$this->isValidComposerPackageName($nameRaw)) {
$output->writeln($this->pluginMsg('bad_name', ['{name}' => (string)$nameRaw]));
return Command::FAILURE;
}
$output->writeln($this->pluginMsg('disable_title', ['{name}' => $nameRaw]));
$configFile = config_path() . "/plugin/{$nameRaw}/app.php";
$output->writeln($this->pluginMsg('config_file', ['{path}' => $this->toRelativePath($configFile)]));
$res = $this->setPluginEnableFlag($configFile, false);
if (!$res['ok']) {
$output->writeln($this->pluginMsg('update_failed', ['{error}' => (string)$res['error']]));
return Command::FAILURE;
}
if ($res['missingFile']) {
$output->writeln($this->pluginMsg('config_missing', ['{path}' => $this->toRelativePath($configFile)]));
// Disable is idempotent: missing config means nothing to disable.
$output->writeln($this->pluginMsg('disabled_ok', ['{name}' => $nameRaw]));
return Command::SUCCESS;
}
if ($res['missingKey']) {
$output->writeln($this->pluginMsg('enable_key_missing', ['{path}' => $this->toRelativePath($configFile)]));
$output->writeln($this->pluginMsg('disabled_ok', ['{name}' => $nameRaw]));
return Command::SUCCESS;
}
if ($res['already']) {
$output->writeln($this->pluginMsg('already_disabled'));
$output->writeln($this->pluginMsg('disabled_ok', ['{name}' => $nameRaw]));
return Command::SUCCESS;
}
$output->writeln($this->pluginMsg('updated_ok', ['{path}' => $this->toRelativePath($configFile)]));
$output->writeln($this->pluginMsg('disabled_ok', ['{name}' => $nameRaw]));
return Command::SUCCESS;
}
protected function buildHelpText(): string
{
return Util::selectByLocale(\Webman\Console\Messages::getPluginDisableHelpText());
}
}
@@ -0,0 +1,88 @@
<?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\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Webman\Console\Commands\Concerns\PluginCommandHelpers;
use Webman\Console\Util;
#[AsCommand('plugin:enable', 'Enable plugin by name')]
class PluginEnableCommand extends Command
{
use PluginCommandHelpers;
/**
* @return void
*/
protected function configure()
{
// Do NOT use "-n": Symfony Console already reserves "-n" for "--no-interaction".
$this->addArgument('name', InputArgument::OPTIONAL, $this->pluginMsg('description_name'));
$this->addOption('name', null, InputOption::VALUE_REQUIRED, $this->pluginMsg('description_name'));
$this->setHelp($this->buildHelpText());
$this->addUsage('foo/my-admin');
$this->addUsage('--name foo/my-admin');
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$nameArg = $this->normalizePluginName($input->getArgument('name'));
$nameOpt = $this->normalizePluginName($input->getOption('name'));
if ($nameArg && $nameOpt && $nameArg !== $nameOpt) {
$output->writeln($this->pluginMsg('name_conflict', ['{arg}' => $nameArg, '{opt}' => $nameOpt]));
return Command::FAILURE;
}
$nameRaw = $nameOpt ?: $nameArg;
if (!$nameRaw) {
$output->writeln($this->pluginMsg('missing_name'));
return Command::FAILURE;
}
if (!$this->isValidComposerPackageName($nameRaw)) {
$output->writeln($this->pluginMsg('bad_name', ['{name}' => (string)$nameRaw]));
return Command::FAILURE;
}
$output->writeln($this->pluginMsg('enable_title', ['{name}' => $nameRaw]));
$configFile = config_path() . "/plugin/{$nameRaw}/app.php";
$output->writeln($this->pluginMsg('config_file', ['{path}' => $this->toRelativePath($configFile)]));
$res = $this->setPluginEnableFlag($configFile, true);
if (!$res['ok']) {
$output->writeln($this->pluginMsg('update_failed', ['{error}' => (string)$res['error']]));
return Command::FAILURE;
}
if ($res['missingFile']) {
$output->writeln($this->pluginMsg('config_missing', ['{path}' => $this->toRelativePath($configFile)]));
return Command::FAILURE;
}
if ($res['missingKey']) {
$output->writeln($this->pluginMsg('config_key_missing', ['{key}' => 'enable', '{path}' => $this->toRelativePath($configFile)]));
return Command::FAILURE;
}
if ($res['already']) {
$output->writeln($this->pluginMsg('already_enabled'));
$output->writeln($this->pluginMsg('enabled_ok', ['{name}' => $nameRaw]));
return Command::SUCCESS;
}
$output->writeln($this->pluginMsg('updated_ok', ['{path}' => $this->toRelativePath($configFile)]));
$output->writeln($this->pluginMsg('enabled_ok', ['{name}' => $nameRaw]));
return Command::SUCCESS;
}
protected function buildHelpText(): string
{
return Util::selectByLocale(\Webman\Console\Messages::getPluginEnableHelpText());
}
}
@@ -0,0 +1,194 @@
<?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\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Webman\Console\Util;
use Webman\Console\Commands\Concerns\PluginCommandHelpers;
#[AsCommand('plugin:export', 'Plugin export')]
class PluginExportCommand extends Command
{
use PluginCommandHelpers;
/**
* @return void
*/
protected function configure()
{
// Do NOT use "-n": Symfony Console already reserves "-n" for "--no-interaction".
$this->addArgument('name', InputArgument::OPTIONAL, $this->pluginMsg('description_name'));
$this->addOption('name', null, InputOption::VALUE_REQUIRED, $this->pluginMsg('description_name'));
$this->addOption('source', 's', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, $this->pluginMsg('description_source'));
$this->setHelp($this->buildHelpText());
$this->addUsage('foo/my-admin --source app --source config');
$this->addUsage('--name foo/my-admin --source app --source config');
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$nameArg = $this->normalizePluginName($input->getArgument('name'));
$nameOpt = $this->normalizePluginName($input->getOption('name'));
if ($nameArg && $nameOpt && $nameArg !== $nameOpt) {
$output->writeln($this->pluginMsg('name_conflict', ['{arg}' => $nameArg, '{opt}' => $nameOpt]));
return Command::FAILURE;
}
$nameRaw = $nameOpt ?: $nameArg;
if (!$nameRaw) {
$output->writeln($this->pluginMsg('missing_name'));
return Command::FAILURE;
}
if (!$this->isValidComposerPackageName($nameRaw)) {
$output->writeln($this->pluginMsg('bad_name', ['{name}' => (string)$nameRaw]));
return Command::FAILURE;
}
$output->writeln($this->pluginMsg('export_title', ['{name}' => $nameRaw]));
$namespace = Util::nameToNamespace($nameRaw);
$pathRelations = $input->getOption('source');
$pathRelations = is_array($pathRelations) ? $pathRelations : [];
$pathRelations = array_values(array_filter(array_map('trim', $pathRelations), static fn($v) => $v !== ''));
$pluginConfigDir = "config/plugin/{$nameRaw}";
if (!in_array($pluginConfigDir, $pathRelations, true) && is_dir($pluginConfigDir)) {
$pathRelations[] = $pluginConfigDir;
}
$originalDest = base_path() . "/vendor/{$nameRaw}";
$dest = $originalDest . '/src';
$this->writeInstallFile($namespace, $pathRelations, $dest);
$output->writeln($this->pluginMsg('export_install_created', ['{path}' => $this->toRelativePath($dest . '/Install.php')]));
foreach ($pathRelations as $source) {
$source = $this->normalizeRelativePath((string)$source);
if ($source === '' || (!is_dir($source) && !is_file($source))) {
$output->writeln($this->pluginMsg('export_skip_missing', ['{path}' => $source]));
continue;
}
$basePath = pathinfo("{$dest}/{$source}", PATHINFO_DIRNAME);
if (!is_dir($basePath)) {
mkdir($basePath, 0777, true);
}
$output->writeln($this->pluginMsg('export_copy', [
'{src}' => $source,
'{dest}' => $this->toRelativePath("{$dest}/{$source}"),
]));
copy_dir($source, "{$dest}/{$source}");
}
$output->writeln($this->pluginMsg('export_saved', [
'{name}' => $nameRaw,
'{dest}' => $this->toRelativePath($originalDest),
]));
return Command::SUCCESS;
}
/**
* @param $namespace
* @param $path_relations
* @param $dest_dir
* @return void
*/
protected function writeInstallFile($namespace, $path_relations, $dest_dir)
{
if (!is_dir($dest_dir)) {
mkdir($dest_dir, 0777, true);
}
$relations = [];
foreach($path_relations as $relation) {
$relations[$relation] = $relation;
}
$relations = var_export($relations, true);
$install_php_content = <<<EOT
<?php
namespace $namespace;
class Install
{
const WEBMAN_PLUGIN = true;
/**
* @var array
*/
protected static \$pathRelation = $relations;
/**
* Install
* @return void
*/
public static function install()
{
static::installByRelation();
}
/**
* Uninstall
* @return void
*/
public static function uninstall()
{
self::uninstallByRelation();
}
/**
* installByRelation
* @return void
*/
public static function installByRelation()
{
foreach (static::\$pathRelation as \$source => \$dest) {
if (\$pos = strrpos(\$dest, '/')) {
\$parent_dir = base_path().'/'.substr(\$dest, 0, \$pos);
if (!is_dir(\$parent_dir)) {
mkdir(\$parent_dir, 0777, true);
}
}
//symlink(__DIR__ . "/\$source", base_path()."/\$dest");
copy_dir(__DIR__ . "/\$source", base_path()."/\$dest");
echo "Create \$dest\r\n";
}
}
/**
* uninstallByRelation
* @return void
*/
public static function uninstallByRelation()
{
foreach (static::\$pathRelation as \$source => \$dest) {
\$path = base_path()."/\$dest";
if (!is_dir(\$path) && !is_file(\$path)) {
continue;
}
echo "Remove \$dest\r\n";
if (is_file(\$path) || is_link(\$path)) {
unlink(\$path);
continue;
}
remove_dir(\$path);
}
}
}
EOT;
file_put_contents("$dest_dir/Install.php", $install_php_content);
}
protected function buildHelpText(): string
{
return Util::selectByLocale(\Webman\Console\Messages::getPluginExportHelpText());
}
}
@@ -0,0 +1,88 @@
<?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\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Webman\Console\Util;
use Webman\Console\Commands\Concerns\PluginCommandHelpers;
#[AsCommand('plugin:install', 'Execute plugin installation script')]
class PluginInstallCommand extends Command
{
use PluginCommandHelpers;
/**
* @return void
*/
protected function configure()
{
// Do NOT use "-n": Symfony Console already reserves "-n" for "--no-interaction".
$this->addArgument('name', InputArgument::OPTIONAL, $this->pluginMsg('description_name'));
$this->addOption('name', null, InputOption::VALUE_REQUIRED, $this->pluginMsg('description_name'));
$this->setHelp($this->buildHelpText());
$this->addUsage('foo/my-admin');
$this->addUsage('--name foo/my-admin');
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$nameArg = $this->normalizePluginName($input->getArgument('name'));
$nameOpt = $this->normalizePluginName($input->getOption('name'));
if ($nameArg && $nameOpt && $nameArg !== $nameOpt) {
$output->writeln($this->pluginMsg('name_conflict', ['{arg}' => $nameArg, '{opt}' => $nameOpt]));
return Command::FAILURE;
}
$nameRaw = $nameOpt ?: $nameArg;
if (!$nameRaw) {
$output->writeln($this->pluginMsg('missing_name'));
return Command::FAILURE;
}
if (!$this->isValidComposerPackageName($nameRaw)) {
$output->writeln($this->pluginMsg('bad_name', ['{name}' => (string)$nameRaw]));
return Command::FAILURE;
}
if (!$this->pluginPackageExists($nameRaw)) {
$output->writeln($this->pluginMsg('plugin_not_found', [
'{name}' => $nameRaw,
'{path}' => "vendor/{$nameRaw}",
]));
return Command::FAILURE;
}
$output->writeln($this->pluginMsg('install_title', ['{name}' => $nameRaw]));
$namespace = Util::nameToNamespace($nameRaw);
$installFunction = "\\{$namespace}\\Install::install";
$pluginConst = "\\{$namespace}\\Install::WEBMAN_PLUGIN";
if (!defined($pluginConst) || !is_callable($installFunction)) {
$output->writeln($this->pluginMsg('script_missing'));
return Command::SUCCESS;
}
try {
$installFunction();
} catch (\Throwable $e) {
$output->writeln($this->pluginMsg('script_failed', ['{error}' => $e->getMessage()]));
return Command::FAILURE;
}
$output->writeln($this->pluginMsg('script_ok'));
return Command::SUCCESS;
}
protected function buildHelpText(): string
{
return Util::selectByLocale(\Webman\Console\Messages::getPluginInstallHelpText());
}
}
@@ -0,0 +1,88 @@
<?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\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Webman\Console\Util;
use Webman\Console\Commands\Concerns\PluginCommandHelpers;
#[AsCommand('plugin:uninstall', 'Execute plugin uninstall script')]
class PluginUninstallCommand extends Command
{
use PluginCommandHelpers;
/**
* @return void
*/
protected function configure()
{
// Do NOT use "-n": Symfony Console already reserves "-n" for "--no-interaction".
$this->addArgument('name', InputArgument::OPTIONAL, $this->pluginMsg('description_name'));
$this->addOption('name', null, InputOption::VALUE_REQUIRED, $this->pluginMsg('description_name'));
$this->setHelp($this->buildHelpText());
$this->addUsage('foo/my-admin');
$this->addUsage('--name foo/my-admin');
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$nameArg = $this->normalizePluginName($input->getArgument('name'));
$nameOpt = $this->normalizePluginName($input->getOption('name'));
if ($nameArg && $nameOpt && $nameArg !== $nameOpt) {
$output->writeln($this->pluginMsg('name_conflict', ['{arg}' => $nameArg, '{opt}' => $nameOpt]));
return Command::FAILURE;
}
$nameRaw = $nameOpt ?: $nameArg;
if (!$nameRaw) {
$output->writeln($this->pluginMsg('missing_name'));
return Command::FAILURE;
}
if (!$this->isValidComposerPackageName($nameRaw)) {
$output->writeln($this->pluginMsg('bad_name', ['{name}' => (string)$nameRaw]));
return Command::FAILURE;
}
if (!$this->pluginPackageExists($nameRaw)) {
$output->writeln($this->pluginMsg('plugin_not_found', [
'{name}' => $nameRaw,
'{path}' => "vendor/{$nameRaw}",
]));
return Command::FAILURE;
}
$output->writeln($this->pluginMsg('uninstall_title', ['{name}' => $nameRaw]));
$namespace = Util::nameToNamespace($nameRaw);
$uninstallFunction = "\\{$namespace}\\Install::uninstall";
$pluginConst = "\\{$namespace}\\Install::WEBMAN_PLUGIN";
if (!defined($pluginConst) || !is_callable($uninstallFunction)) {
$output->writeln($this->pluginMsg('script_missing'));
return Command::SUCCESS;
}
try {
$uninstallFunction();
} catch (\Throwable $e) {
$output->writeln($this->pluginMsg('script_failed', ['{error}' => $e->getMessage()]));
return Command::FAILURE;
}
$output->writeln($this->pluginMsg('script_ok'));
return Command::SUCCESS;
}
protected function buildHelpText(): string
{
return Util::selectByLocale(\Webman\Console\Messages::getPluginUninstallHelpText());
}
}
+37
View File
@@ -0,0 +1,37 @@
<?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 Webman\Console\Commands\Concerns\ServiceCommandExecutor;
use Webman\Console\Messages;
use Webman\Console\Util;
#[AsCommand('restart', 'Restart workers. Use mode -d to start in DAEMON mode. Use mode -g to stop gracefully.')]
class ReStartCommand extends Command
{
use ServiceCommandExecutor;
protected function configure() : void
{
$messages = Util::selectLocaleMessages(Messages::getServiceMessages());
$this->setDescription($messages['restart_desc']);
$this
->addOption('daemon', 'd', InputOption::VALUE_NONE, $messages['daemon_option'])
->addOption('graceful', 'g', InputOption::VALUE_NONE, $messages['graceful_stop']);
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
return $this->executeServiceCommand();
}
}
+35
View File
@@ -0,0 +1,35 @@
<?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 Webman\Console\Commands\Concerns\ServiceCommandExecutor;
use Webman\Console\Messages;
use Webman\Console\Util;
#[AsCommand('reload', 'Reload codes. Use mode -g to reload gracefully.')]
class ReloadCommand extends Command
{
use ServiceCommandExecutor;
protected function configure() : void
{
$messages = Util::selectLocaleMessages(Messages::getServiceMessages());
$this->setDescription($messages['reload_desc']);
$this->addOption('graceful', 'g', InputOption::VALUE_NONE, $messages['graceful_reload']);
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
return $this->executeServiceCommand();
}
}
+59
View File
@@ -0,0 +1,59 @@
<?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\Helper\Table;
use Webman\Console\Commands\Concerns\BaseCommandHelpers;
use Webman\Console\Util;
use Webman\Console\Messages;
use Webman\Route;
#[AsCommand('route:list', 'Route list')]
class RouteListCommand extends Command
{
use BaseCommandHelpers;
protected function configure(): void
{
$desc = $this->msg('desc');
$this->setDescription($desc);
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln($this->msg('title'));
$headers = $this->msg('headers');
$closureLabel = $this->msg('closure_label');
$rows = [];
foreach (Route::getRoutes() as $route) {
foreach ($route->getMethods() as $method) {
$cb = $route->getCallback();
$cb = $cb instanceof \Closure
? $closureLabel
: (is_array($cb) ? json_encode($cb) : var_export($cb, 1));
$rows[] = [$route->getPath(), $method, $cb, json_encode($route->getMiddleware() ?: null), $route->getName()];
}
}
$table = new Table($output);
$table->setHeaders($headers);
$table->setRows($rows);
$table->render();
return Command::SUCCESS;
}
protected function msg(string $key, array $replace = []): mixed
{
$messages = Util::selectLocaleMessages(Messages::getRouteListMessages());
return $this->getLocalizedMessage($messages, $key, $replace);
}
}
+35
View File
@@ -0,0 +1,35 @@
<?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 Webman\Console\Commands\Concerns\ServiceCommandExecutor;
use Webman\Console\Messages;
use Webman\Console\Util;
#[AsCommand('start', 'Start worker in DEBUG mode. Use mode -d to start in DAEMON mode.')]
class StartCommand extends Command
{
use ServiceCommandExecutor;
protected function configure() : void
{
$messages = Util::selectLocaleMessages(Messages::getServiceMessages());
$this->setDescription($messages['start_desc']);
$this->addOption('daemon', 'd', InputOption::VALUE_NONE, $messages['daemon_option']);
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
return $this->executeServiceCommand();
}
}
+35
View File
@@ -0,0 +1,35 @@
<?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 Webman\Console\Commands\Concerns\ServiceCommandExecutor;
use Webman\Console\Messages;
use Webman\Console\Util;
#[AsCommand('status', 'Get worker status. Use mode -d to show live status.')]
class StatusCommand extends Command
{
use ServiceCommandExecutor;
protected function configure() : void
{
$messages = Util::selectLocaleMessages(Messages::getServiceMessages());
$this->setDescription($messages['status_desc']);
$this->addOption('live', 'd', InputOption::VALUE_NONE, $messages['live_status']);
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
return $this->executeServiceCommand();
}
}
+35
View File
@@ -0,0 +1,35 @@
<?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 Webman\Console\Commands\Concerns\ServiceCommandExecutor;
use Webman\Console\Messages;
use Webman\Console\Util;
#[AsCommand('stop', 'Stop worker. Use mode -g to stop gracefully.')]
class StopCommand extends Command
{
use ServiceCommandExecutor;
protected function configure() : void
{
$messages = Util::selectLocaleMessages(Messages::getServiceMessages());
$this->setDescription($messages['stop_desc']);
$this->addOption('graceful', 'g',InputOption::VALUE_NONE, $messages['graceful_stop']);
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
return $this->executeServiceCommand();
}
}
+51
View File
@@ -0,0 +1,51 @@
<?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 Webman\Console\Commands\Concerns\BaseCommandHelpers;
use Webman\Console\Messages;
use Webman\Console\Util;
#[AsCommand('version', 'Show webman version')]
class VersionCommand extends Command
{
use BaseCommandHelpers;
protected function configure(): void
{
$messages = Util::selectLocaleMessages(Messages::getVersionMessages());
$this->setDescription($messages['desc']);
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$version_info = [];
$installed_file = base_path() . '/vendor/composer/installed.php';
if (is_file($installed_file)) {
$version_info = include $installed_file;
}
$webman_framework_version = $version_info['versions']['workerman/webman-framework']['pretty_version'] ?? null;
$webman_framework_version = is_string($webman_framework_version) ? trim($webman_framework_version) : '';
if ($webman_framework_version === '') {
$output->writeln($this->msg('not_found'));
return Command::FAILURE;
}
$output->writeln($this->msg('version', ['{version}' => $webman_framework_version]));
return Command::SUCCESS;
}
protected function msg(string $key, array $replace = []): string
{
$messages = Util::selectLocaleMessages(Messages::getVersionMessages());
return $this->getLocalizedMessage($messages, $key, $replace);
}
}
+76
View File
@@ -0,0 +1,76 @@
<?php
namespace Webman\Console;
class Install
{
const WEBMAN_PLUGIN = true;
/**
* @var array
*/
protected static $pathRelation = [
'config/plugin/webman/console' => 'config/plugin/webman/console',
];
/**
* Install
* @return void
*/
public static function install()
{
$dest = base_path() . "/webman";
if (is_dir($dest)) {
echo "Installation failed, please remove directory $dest\n";
return;
}
copy(__DIR__ . "/webman", $dest);
chmod(base_path() . "/webman", 0755);
static::installByRelation();
}
/**
* Uninstall
* @return void
*/
public static function uninstall()
{
if (is_file(base_path()."/webman")) {
unlink(base_path() . "/webman");
}
self::uninstallByRelation();
}
/**
* installByRelation
* @return void
*/
public static function installByRelation()
{
foreach (static::$pathRelation as $source => $dest) {
if ($pos = strrpos($dest, '/')) {
$parent_dir = base_path().'/'.substr($dest, 0, $pos);
if (!is_dir($parent_dir)) {
mkdir($parent_dir, 0777, true);
}
}
copy_dir(__DIR__ . "/$source", base_path()."/$dest");
}
}
/**
* uninstallByRelation
* @return void
*/
public static function uninstallByRelation()
{
foreach (static::$pathRelation as $source => $dest) {
$path = base_path()."/$dest";
if (!is_dir($path) && !is_file($path)) {
continue;
}
remove_dir($path);
}
}
}
File diff suppressed because it is too large Load Diff
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace Webman\Console;
final class OrmType
{
public const LARAVEL = 'laravel';
public const THINKORM = 'thinkorm';
}
+495
View File
@@ -0,0 +1,495 @@
<?php
namespace Webman\Console;
use Doctrine\Inflector\InflectorFactory;
class Util
{
/**
* Get current locale for CLI messages. Default is en.
* All commands can use this to get the current language for prompts.
*
* @return string
*/
public static function getLocale(): string
{
$locale = 'en';
if (function_exists('config')) {
$value = config('translation.locale', 'en');
$value = is_string($value) ? trim($value) : '';
if ($value !== '') {
$locale = $value;
}
}
return $locale;
}
/**
* Select message map by current locale. Fallback: exact -> language prefix -> en -> zh_CN -> first.
*
* @param array<string, array<string, string>> $localeToMessages e.g. ['zh_CN' => ['key' => '...'], 'en' => [...]]
* @return array<string, string>
*/
public static function selectLocaleMessages(array $localeToMessages): array
{
$locale = self::getLocale();
if (isset($localeToMessages[$locale])) {
return $localeToMessages[$locale];
}
$lang = explode('_', $locale)[0] ?? '';
if ($lang !== '' && isset($localeToMessages[$lang])) {
return $localeToMessages[$lang];
}
// Use configured fallback locales if available, otherwise default to ['en'].
$fallbacks = ['en'];
if (function_exists('config')) {
$cfg = config('translation.fallback_locale', $fallbacks);
if (is_string($cfg)) {
$cfg = [$cfg];
}
if (is_array($cfg) && !empty($cfg)) {
$fallbacks = $cfg;
}
}
foreach ($fallbacks as $fb) {
if (isset($localeToMessages[$fb])) {
return $localeToMessages[$fb];
}
}
$first = reset($localeToMessages);
return is_array($first) ? $first : [];
}
/**
* Select one value by current locale. Fallback: exact -> language prefix -> en -> zh_CN -> first.
*
* @param array<string, string> $localeToValue e.g. ['zh_CN' => '简体', 'en' => 'English']
* @return string
*/
public static function selectByLocale(array $localeToValue): string
{
$locale = self::getLocale();
if (isset($localeToValue[$locale])) {
return $localeToValue[$locale];
}
$lang = explode('_', $locale)[0] ?? '';
if ($lang !== '' && isset($localeToValue[$lang])) {
return $localeToValue[$lang];
}
// Use configured fallback locales if available, otherwise default to ['en'].
$fallbacks = ['en'];
if (function_exists('config')) {
$cfg = config('translation.fallback_locale', $fallbacks);
if (is_string($cfg)) {
$cfg = [$cfg];
}
if (is_array($cfg) && !empty($cfg)) {
$fallbacks = $cfg;
}
}
foreach ($fallbacks as $fb) {
if (isset($localeToValue[$fb])) {
return $localeToValue[$fb];
}
}
$first = reset($localeToValue);
return is_string($first) ? $first : '';
}
/**
* Select an array by current locale (e.g. table headers). Fallback: exact -> language prefix -> en -> zh_CN -> first.
*
* @param array<string, array> $localeToArray e.g. ['zh_CN' => ['A','B'], 'en' => ['A','B']]
* @return array
*/
public static function selectLocaleArray(array $localeToArray): array
{
$locale = self::getLocale();
if (isset($localeToArray[$locale])) {
return $localeToArray[$locale];
}
$lang = explode('_', $locale)[0] ?? '';
if ($lang !== '' && isset($localeToArray[$lang])) {
return $localeToArray[$lang];
}
// Use configured fallback locales if available, otherwise default to ['en'].
$fallbacks = ['en'];
if (function_exists('config')) {
$cfg = config('translation.fallback_locale', $fallbacks);
if (is_string($cfg)) {
$cfg = [$cfg];
}
if (is_array($cfg) && !empty($cfg)) {
$fallbacks = $cfg;
}
}
foreach ($fallbacks as $fb) {
if (isset($localeToArray[$fb])) {
return $localeToArray[$fb];
}
}
$first = reset($localeToArray);
return is_array($first) ? $first : [];
}
/**
* Execute an external shell command using the best available PHP function.
*
* Non-interactive (default): captures output, stdin from null device to prevent blocking.
* Tries: proc_open -> exec -> system -> passthru -> popen
*
* Interactive ($interactive = true): passes I/O through to the terminal so the user
* can see prompts and respond. Output is not captured (returned as empty string).
* Tries: passthru -> system -> proc_open
* If no interactive-capable function is available, returns null.
*
* @param string $command
* @param bool $interactive
* @return array{output: string, exit_code: int}|null null if no suitable function is available
*/
public static function exec(string $command, bool $interactive = false): ?array
{
return $interactive
? static::execInteractive($command)
: static::execCapture($command);
}
/**
* Run a command with terminal passthrough — user can see output and respond to prompts.
*/
protected static function execInteractive(string $command): ?array
{
if (static::isCallable('passthru')) {
$code = 0;
passthru($command, $code);
return ['output' => '', 'exit_code' => $code];
}
if (static::isCallable('system')) {
$code = 0;
system($command, $code);
return ['output' => '', 'exit_code' => $code];
}
if (static::isCallable('proc_open') && defined('STDIN')) {
$process = proc_open($command, [STDIN, STDOUT, STDERR], $pipes);
if (is_resource($process)) {
return ['output' => '', 'exit_code' => proc_close($process)];
}
}
return null;
}
/**
* Run a command non-interactively — capture output, stdin from null device to prevent blocking.
*/
protected static function execCapture(string $command): ?array
{
$nullDevice = PHP_OS_FAMILY === 'Windows' ? 'NUL' : '/dev/null';
if (static::isCallable('proc_open')) {
$process = proc_open($command, [
0 => ['file', $nullDevice, 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
], $pipes);
if (is_resource($process)) {
$stdout = (string)stream_get_contents($pipes[1]);
$stderr = (string)stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$exitCode = proc_close($process);
return ['output' => trim($stdout . $stderr), 'exit_code' => $exitCode];
}
}
$cmd = "$command < $nullDevice 2>&1";
if (static::isCallable('exec')) {
$lines = [];
$code = 0;
exec($cmd, $lines, $code);
return ['output' => implode("\n", $lines), 'exit_code' => $code];
}
if (static::isCallable('system')) {
ob_start();
$code = 0;
system($cmd, $code);
return ['output' => trim((string)ob_get_clean()), 'exit_code' => $code];
}
if (static::isCallable('passthru')) {
ob_start();
$code = 0;
passthru($cmd, $code);
return ['output' => trim((string)ob_get_clean()), 'exit_code' => $code];
}
if (static::isCallable('popen')) {
$handle = popen($cmd, 'r');
if ($handle !== false) {
$output = (string)stream_get_contents($handle);
$exitCode = pclose($handle);
return ['output' => trim($output), 'exit_code' => $exitCode];
}
}
return null;
}
/**
* Check whether a function exists and is not disabled.
*
* @param string $function
* @return bool
*/
public static function isCallable(string $function): bool
{
return function_exists($function) && is_callable($function);
}
public static function nameToNamespace($name)
{
$namespace = ucfirst($name);
$namespace = preg_replace_callback(['/-([a-zA-Z])/', '/(\/[a-zA-Z])/'], function ($matches) {
return strtoupper($matches[1]);
}, $namespace);
return str_replace('/', '\\' ,ucfirst($namespace));
}
public static function classToName($class)
{
$class = lcfirst($class);
return preg_replace_callback(['/([A-Z])/'], function ($matches) {
return '_' . strtolower($matches[1]);
}, $class);
}
public static function nameToClass($class)
{
$class = preg_replace_callback(['/-([a-zA-Z])/', '/_([a-zA-Z])/'], function ($matches) {
return strtoupper($matches[1]);
}, $class);
if (!($pos = strrpos($class, '/'))) {
$class = ucfirst($class);
} else {
$path = substr($class, 0, $pos);
$class = ucfirst(substr($class, $pos + 1));
$class = "$path/$class";
}
return $class;
}
public static function guessPath($base_path, $name, $return_full_path = false)
{
if (!is_dir($base_path)) {
return false;
}
$names = explode('/', trim(strtolower($name), '/'));
$realname = [];
$path = $base_path;
foreach ($names as $name) {
$finded = false;
foreach (scandir($path) ?: [] as $tmp_name) {
if (strtolower($tmp_name) === $name && is_dir("$path/$tmp_name")) {
$path = "$path/$tmp_name";
$realname[] = $tmp_name;
$finded = true;
break;
}
}
if (!$finded) {
return false;
}
}
$realname = implode(DIRECTORY_SEPARATOR, $realname);
return $return_full_path ? get_realpath($base_path . DIRECTORY_SEPARATOR . $realname) : $realname;
}
/**
* Get the default directory name for a given type (controller, model, etc.)
* by detecting the actual local directory structure.
*
* Priority when multiple matches exist:
* 1. All lowercase (e.g. "controller")
* 2. First-letter uppercase (e.g. "Controller")
* 3. Any other casing found
*
* @param string $type Directory type, e.g. "controller", "model", "middleware", "bootstrap", "command", "process", "validation"
* @param string|null $plugin Plugin name, e.g. "admin"
* @return string The directory name with correct casing, e.g. "controller" or "Controller"
*/
public static function getDefaultAppPath(string $type, ?string $plugin = null): string
{
$type = strtolower(trim($type));
if ($type === '') {
return '';
}
// Determine the base app directory.
if ($plugin) {
$appDirName = self::detectAppDirName($plugin);
$baseDir = base_path('plugin' . DIRECTORY_SEPARATOR . trim($plugin) . DIRECTORY_SEPARATOR . $appDirName);
} else {
$baseDir = app_path();
}
// Try to find the exact directory on disk (case-insensitive).
if (is_dir($baseDir)) {
$matches = [];
foreach (scandir($baseDir) ?: [] as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}
if (strtolower($entry) === $type && is_dir($baseDir . DIRECTORY_SEPARATOR . $entry)) {
$matches[] = $entry;
}
}
if (count($matches) === 1) {
return $matches[0];
}
if (count($matches) > 1) {
// Priority: all lowercase > first-letter uppercase > others
foreach ($matches as $m) {
if ($m === $type) {
return $m; // all lowercase
}
}
$ucfirst = ucfirst($type);
foreach ($matches as $m) {
if ($m === $ucfirst) {
return $m; // first-letter uppercase
}
}
return $matches[0]; // first found
}
}
// No match found. Determine casing convention from existing siblings.
return self::guessAppDirCase($type, $baseDir);
}
/**
* Get the full relative path for a type directory with correct casing.
*
* Examples:
* - getDefaultAppRelativePath('controller') => "app/controller" or "app/Controller"
* - getDefaultAppRelativePath('model', 'admin') => "plugin/admin/app/model" or "plugin/admin/app/Model"
*
* @param string $type
* @param string|null $plugin
* @return string
*/
public static function getDefaultAppRelativePath(string $type, ?string $plugin = null): string
{
$dirName = self::getDefaultAppPath($type, $plugin);
if ($plugin) {
$appDirName = self::detectAppDirName($plugin);
return 'plugin/' . trim($plugin) . '/' . $appDirName . '/' . $dirName;
}
// Detect the actual "app" directory name casing.
$appDirName = self::detectAppDirName($plugin);
return $appDirName . '/' . $dirName;
}
/**
* Convert a relative path to a PHP namespace, preserving the original casing.
*
* Examples:
* - "app/controller" => "app\controller"
* - "App/Controller" => "App\Controller"
* - "plugin/admin/app/controller" => "plugin\admin\app\controller"
*
* @param string $relativePath
* @return string
*/
public static function pathToNamespace(string $relativePath): string
{
$path = trim(str_replace('\\', '/', $relativePath), '/');
return str_replace('/', '\\', $path);
}
/**
* Detect the actual casing of the "app" directory under base_path().
*
* @param string|null $plugin
* @return string "app" or "App" depending on what exists on disk
*/
public static function detectAppDirName(?string $plugin = null): string
{
if ($plugin) {
$parentDir = base_path('plugin' . DIRECTORY_SEPARATOR . trim($plugin));
} else {
$parentDir = base_path();
}
if (!is_dir($parentDir)) {
return 'app';
}
foreach (scandir($parentDir) ?: [] as $entry) {
if (strtolower($entry) === 'app' && is_dir($parentDir . DIRECTORY_SEPARATOR . $entry)) {
return $entry;
}
}
return 'app';
}
/**
* Guess the directory name casing for a type that doesn't yet exist,
* based on sibling directories' naming convention.
*
* Priority: if any siblings are all-lowercase, use lowercase.
* If siblings are first-letter uppercase, use ucfirst.
* Fallback: lowercase.
*
* @param string $type All-lowercase type name, e.g. "model"
* @param string $baseDir The app directory to scan for siblings
* @return string The type name with guessed casing
*/
public static function guessAppDirCase(string $type, string $baseDir): string
{
$knownTypes = ['controller', 'model', 'middleware', 'bootstrap', 'command', 'process', 'validation', 'view', 'functions'];
if (!is_dir($baseDir)) {
return $type;
}
$hasLower = false;
$hasUpper = false;
foreach (scandir($baseDir) ?: [] as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}
if (!is_dir($baseDir . DIRECTORY_SEPARATOR . $entry)) {
continue;
}
$lower = strtolower($entry);
if (!in_array($lower, $knownTypes, true)) {
continue;
}
if ($entry === $lower) {
$hasLower = true;
} else if ($entry === ucfirst($lower)) {
$hasUpper = true;
}
}
// Priority: lowercase > ucfirst > default lowercase
if ($hasLower) {
return $type;
}
if ($hasUpper) {
return ucfirst($type);
}
return $type;
}
}
@@ -0,0 +1,28 @@
<?php
return [
'enable' => true,
'build_dir' => BASE_PATH . DIRECTORY_SEPARATOR . 'build',
'phar_filename' => 'webman.phar',
'phar_format' => Phar::PHAR, // Phar archive format: Phar::PHAR, Phar::TAR, Phar::ZIP
'phar_compression' => Phar::NONE, // Compression method for Phar archive: Phar::NONE, Phar::GZ, Phar::BZ2
'bin_filename' => 'webman.bin',
'signature_algorithm'=> Phar::SHA256, //set the signature algorithm for a phar and apply it. The signature algorithm must be one of Phar::MD5, Phar::SHA1, Phar::SHA256, Phar::SHA512, or Phar::OPENSSL.
'private_key_file' => '', // The file path for certificate or OpenSSL private key file.
'exclude_pattern' => '#^(?!.*(composer.json|/.github/|/.idea/|/.git/|/.setting/|/runtime/|/vendor-bin/|/build/|/vendor/webman/admin/))(.*)$#',
'exclude_files' => [
'.env', 'LICENSE', 'composer.json', 'composer.lock', 'start.php', 'webman.phar', 'webman.bin'
],
'custom_ini' => '
memory_limit = 256M
',
];
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env php
<?php
use Webman\Config;
use Webman\Console\Command;
use Webman\Console\Util;
use support\Container;
use Dotenv\Dotenv;
if (!Phar::running()) {
chdir(__DIR__);
}
require_once __DIR__ . '/vendor/autoload.php';
if (!$appConfigFile = config_path('app.php')) {
throw new RuntimeException('Config file not found: app.php');
}
if (class_exists(Dotenv::class) && file_exists(run_path('.env'))) {
if (method_exists(Dotenv::class, 'createUnsafeImmutable')) {
Dotenv::createUnsafeImmutable(run_path())->load();
} else {
Dotenv::createMutable(run_path())->load();
}
}
$appConfig = require $appConfigFile;
if ($timezone = $appConfig['default_timezone'] ?? '') {
date_default_timezone_set($timezone);
}
if ($errorReporting = $appConfig['error_reporting'] ?? '') {
error_reporting($errorReporting);
}
if (!in_array($argv[1] ?? '', ['start', 'restart', 'stop', 'status', 'reload', 'connections'])) {
require_once __DIR__ . '/support/bootstrap.php';
} else {
if (class_exists('Support\App')) {
Support\App::loadAllConfig(['route']);
} else {
Config::reload(config_path(), ['route', 'container']);
}
}
$cli = new Command();
$cli->setName('webman cli');
$cli->installInternalCommands();
if (is_dir($command_path = Util::guessPath(app_path(), '/command', true))) {
$cli->installCommands($command_path);
}
foreach (config('plugin', []) as $firm => $projects) {
if (isset($projects['app'])) {
foreach (['', '/app'] as $app) {
if ($command_str = Util::guessPath(base_path() . "/plugin/$firm{$app}", 'command')) {
$command_path = base_path() . "/plugin/$firm{$app}/$command_str";
$cli->installCommands($command_path, "plugin\\$firm" . str_replace('/', '\\', $app) . "\\$command_str");
}
}
}
foreach ($projects as $name => $project) {
if (!is_array($project)) {
continue;
}
$project['command'] ??= [];
array_walk($project['command'], [$cli, 'createCommandInstance']);
}
}
$cli->run();
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 webman
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.
+2
View File
@@ -0,0 +1,2 @@
# Webman database
This is Webman's database library, which includes database client and connection pool.
+20
View File
@@ -0,0 +1,20 @@
{
"name": "webman/database",
"type": "library",
"license": "MIT",
"description": "Webman database",
"require": {
"workerman/webman-framework": "^2.1 || dev-master",
"illuminate/database": "^10.0 || ^11.0 || ^12.0 || ^13.0",
"illuminate/http": "^10.0 || ^11.0 || ^12.0 || ^13.0",
"laravel/serializable-closure": "^1.0 || ^2.0"
},
"autoload": {
"psr-4": {
"Webman\\Database\\": "src",
"support\\": "src/support"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}
+156
View File
@@ -0,0 +1,156 @@
<?php
namespace Webman\Database;
use Illuminate\Database\DatabaseManager as BaseDatabaseManager;
use Throwable;
use Webman\Context;
use Workerman\Coroutine\Pool;
/**
* Class DatabaseManager
*/
class DatabaseManager extends BaseDatabaseManager
{
/**
* Default heartbeat SQL (most SQL databases).
*
* @var string
*/
private const HEARTBEAT_SQL_DEFAULT = 'SELECT 1 AS ping';
/**
* Oracle heartbeat SQL.
*
* @var string
*/
private const HEARTBEAT_SQL_ORACLE = 'SELECT 1 AS ping FROM DUAL';
/**
* @var Pool[]
*/
protected static array $pools = [];
/**
* @inheritDoc
*/
public function __construct(...$args)
{
parent::__construct(...$args);
$this->reconnector = function ($connection) {
$name = $connection->getNameWithReadWriteType();
[$database, $type] = $this->parseConnectionName($name);
$fresh = $this->configure(
$this->makeConnection($database), $type
);
$connection->setPdo($fresh->getRawPdo());
};
}
/**
* Get connection
*
* @param $name
* @return mixed
* @throws Throwable
*/
public function connection($name = null): mixed
{
$name = $name ?: $this->getDefaultConnection();
[$database, $type] = $this->parseConnectionName($name);
$key = "database.connections.$name";
$connection = Context::get($key);
if (!$connection) {
if (!isset(static::$pools[$name])) {
$poolConfig = $this->app['config']['database.connections'][$name]['pool'] ?? [];
$pool = new Pool($poolConfig['max_connections'] ?? 6, $poolConfig);
$pool->setConnectionCreator(function () use ($database, $type) {
return $this->configure($this->makeConnection($database), $type);
});
$pool->setConnectionCloser(function ($connection) {
$this->closeAndFreeConnection($connection);
});
$pool->setHeartbeatChecker(function ($connection) {
$this->heartbeat($connection);
});
static::$pools[$name] = $pool;
}
try {
$connection = static::$pools[$name]->get();
Context::set($key, $connection);
} finally {
// We cannot use Coroutine::defer() because we may not be in a coroutine environment currently.
Context::onDestroy(function () use ($connection, $name) {
try {
$connection && static::$pools[$name]->put($connection);
} catch (Throwable) {
// ignore
}
});
}
}
return $connection;
}
/**
* Heartbeat checker for pooled connections.
*
* @param mixed $connection
* @return void
*/
private function heartbeat(mixed $connection): void
{
$driver = strtolower((string)$connection->getDriverName());
// MongoDB (mongodb/laravel-mongodb or jenssegers/mongodb)
if ($driver === 'mongodb') {
$connection->command(['ping' => 1]);
return;
}
$sql = $this->getHeartbeatSql($driver);
if ($sql !== '') {
$connection->select($sql);
}
}
/**
* Get heartbeat SQL by driver name.
*
* Illuminate\Database supports mysql/mariadb/pgsql/sqlite/sqlsrv by default.
* Oracle (oracle/oci/oci8) may be provided by third-party drivers.
*
* @param string $driver
* @return string
*/
private function getHeartbeatSql(string $driver): string
{
return match ($driver) {
'oracle', 'oci', 'oci8' => self::HEARTBEAT_SQL_ORACLE,
default => self::HEARTBEAT_SQL_DEFAULT,
};
}
/**
* Close connection.
*
* @param $connection
* @return void
*/
protected function closeAndFreeConnection($connection): void
{
// Remove connection from Context
$name = $connection->getNameWithReadWriteType();
$key = "database.connections.$name";
Context::set($key, null);
$connection->disconnect();
$clearProperties = function () {
$this->queryGrammar = null;
};
$clearProperties->call($connection);
}
}
+113
View File
@@ -0,0 +1,113 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman\Database;
use Illuminate\Container\Container as IlluminateContainer;
use Webman\Database\Manager as Capsule;
use Illuminate\Events\Dispatcher;
use Illuminate\Pagination\Paginator;
use Illuminate\Pagination\CursorPaginator;
use Illuminate\Pagination\Cursor;
use Jenssegers\Mongodb\Connection as JenssegersMongodbConnection;
use MongoDB\Laravel\Connection as LaravelMongodbConnection;
use support\Container;
/**
* Class Initializer
*/
class Initializer
{
/**
* @var bool
*/
private static bool $initialized = false;
/**
* @param $config
* @return void
*/
public static function init($config): void
{
if (self::$initialized) {
return;
}
self::$initialized = true;
$connections = $config['connections'] ?? [];
if (!$connections) {
return;
}
$capsule = new Capsule(IlluminateContainer::getInstance());
$capsule->getDatabaseManager()->extend('mongodb', function ($config, $name) {
$config['name'] = $name;
return class_exists(LaravelMongodbConnection::class) ? new LaravelMongodbConnection($config) : new JenssegersMongodbConnection($config);
});
$default = $config['default'] ?? false;
if ($default) {
$defaultConfig = $connections[$default] ?? false;
if ($defaultConfig) {
$capsule->addConnection($defaultConfig, $default);
$capsule->getDatabaseManager()->setDefaultConnection($default);
unset($connections[$default]);
}
}
foreach ($connections as $name => $config) {
$capsule->addConnection($config, $name);
}
if (class_exists(Dispatcher::class) && !$capsule->getEventDispatcher()) {
$capsule->setEventDispatcher(Container::make(Dispatcher::class, [IlluminateContainer::getInstance()]));
}
$capsule->setAsGlobal();
$capsule->bootEloquent();
// Paginator
if (class_exists(Paginator::class)) {
if (method_exists(Paginator::class, 'queryStringResolver')) {
Paginator::queryStringResolver(function () {
$request = request();
return $request?->queryString();
});
}
Paginator::currentPathResolver(function () {
$request = request();
return $request ? $request->path(): '/';
});
Paginator::currentPageResolver(function ($pageName = 'page') {
$request = request();
if (!$request) {
return 1;
}
$page = (int)($request->input($pageName, 1));
return $page > 0 ? $page : 1;
});
if (class_exists(CursorPaginator::class)) {
CursorPaginator::currentCursorResolver(function ($cursorName = 'cursor') {
return Cursor::fromEncoded(request()->input($cursorName));
});
}
}
}
}
Initializer::init(config('database', []));
+91
View File
@@ -0,0 +1,91 @@
<?php
namespace Webman\Database;
class Install
{
const WEBMAN_PLUGIN = true;
/**
* @var array
*/
protected static array $pathRelation = [];
/**
* Install
* @return void
*/
public static function install(): void
{
$database_file = config_path('database.php');
if (!is_file($database_file)) {
echo 'Create config/database.php' . PHP_EOL;
copy(__DIR__ . '/config/database.php', $database_file);
}
static::installByRelation();
static::removeLaravelDbFromBootstrap();
}
/**
* @return void
*/
protected static function removeLaravelDbFromBootstrap(): void
{
$file = base_path('config/bootstrap.php');
if (!is_file($file)) {
return;
}
$pattern = '/^\s*support\\\\bootstrap\\\\LaravelDb::class,\s*?\r?\n/m';
$content = file_get_contents($file);
if (preg_match($pattern, $content)) {
$updatedContent = preg_replace($pattern, '', $content);
file_put_contents($file, $updatedContent);
}
}
/**
* Uninstall
* @return void
*/
public static function uninstall(): void
{
$database_file = config_path('database.php');
if (is_file($database_file)) {
echo 'Remove config/database.php' . PHP_EOL;
unlink($database_file);
}
self::uninstallByRelation();
}
/**
* installByRelation
* @return void
*/
public static function installByRelation(): void
{
foreach (static::$pathRelation as $source => $dest) {
if ($pos = strrpos($dest, '/')) {
$parent_dir = base_path().'/'.substr($dest, 0, $pos);
if (!is_dir($parent_dir)) {
mkdir($parent_dir, 0777, true);
}
}
copy_dir(__DIR__ . "/$source", base_path()."/$dest");
}
}
/**
* uninstallByRelation
* @return void
*/
public static function uninstallByRelation(): void
{
foreach (static::$pathRelation as $source => $dest) {
$path = base_path()."/$dest";
if (!is_dir($path) && !is_file($path)) {
continue;
}
remove_dir($path);
}
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace Webman\Database;
use Illuminate\Database\Connectors\ConnectionFactory;
/**
* Class Manager
*/
class Manager extends \Illuminate\Database\Capsule\Manager
{
/**
* Build the database manager instance.
*
* @return void
*/
protected function setupManager()
{
$factory = new ConnectionFactory($this->container);
$this->manager = new DatabaseManager($this->container, $factory);
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
return [
'default' => 'mysql',
'connections' => [
'mysql' => [
'driver' => 'mysql',
'host' => '127.0.0.1',
'port' => '3306',
'database' => 'your_database',
'username' => 'your_username',
'password' => 'your_password',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_general_ci',
'prefix' => '',
'strict' => true,
'engine' => null,
'options' => [
PDO::ATTR_EMULATE_PREPARES => false, // Must be false for Swoole and Swow drivers.
],
'pool' => [
'max_connections' => 5,
'min_connections' => 1,
'wait_timeout' => 3,
'idle_timeout' => 60,
'heartbeat_interval' => 50,
],
],
],
];
+40
View File
@@ -0,0 +1,40 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace support;
use Closure;
use Webman\Database\Manager;
use Illuminate\Contracts\Database\Query\Expression;
require_once __DIR__ . '/../Initializer.php';
/**
* Class Db
* @package support
* @method static array select(string $query, $bindings = [], $useReadPdo = true)
* @method static int insert(string $query, $bindings = [])
* @method static int update(string $query, $bindings = [])
* @method static int delete(string $query, $bindings = [])
* @method static bool statement(string $query, $bindings = [])
* @method static mixed transaction(Closure $callback, $attempts = 1)
* @method static void beginTransaction()
* @method static void rollBack($toLevel = null)
* @method static void commit()
* @method static Expression raw(mixed $value)
*/
class Db extends Manager
{
}
+264
View File
@@ -0,0 +1,264 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace support;
use Closure;
use Illuminate\Contracts\Pagination\CursorPaginator;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Contracts\Pagination\Paginator;
use Illuminate\Database\Eloquent\Model as BaseModel;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\Expression;
use Illuminate\Database\Query\Grammars\Grammar;
use Illuminate\Database\Query\Processors\Processor;
use Illuminate\Support\Collection;
use Illuminate\Support\LazyCollection;
require_once __DIR__ . '/../Initializer.php';
/**
* @method static BaseModel make($attributes = [])
* @method static \Illuminate\Database\Eloquent\Builder|static withGlobalScope($identifier, $scope)
* @method static \Illuminate\Database\Eloquent\Builder|static withoutGlobalScope($scope)
* @method static \Illuminate\Database\Eloquent\Builder|static withoutGlobalScopes($scopes = null)
* @method static array removedScopes()
* @method static \Illuminate\Database\Eloquent\Builder|static whereKey($id)
* @method static \Illuminate\Database\Eloquent\Builder|static whereKeyNot($id)
* @method static \Illuminate\Database\Eloquent\Builder|static where($column, $operator = null, $value = null, $boolean = 'and')
* @method static BaseModel|null firstWhere($column, $operator = null, $value = null, $boolean = 'and')
* @method static \Illuminate\Database\Eloquent\Builder|static orWhere($column, $operator = null, $value = null)
* @method static \Illuminate\Database\Eloquent\Builder|static latest($column = null)
* @method static \Illuminate\Database\Eloquent\Builder|static oldest($column = null)
* @method static \Illuminate\Database\Eloquent\Collection|static hydrate($items)
* @method static \Illuminate\Database\Eloquent\Collection|static fromQuery($query, $bindings = [])
* @method static BaseModel|\Illuminate\Database\Eloquent\Collection|static[]|static|null find($id, $columns = [])
* @method static \Illuminate\Database\Eloquent\Collection|static findMany($ids, $columns = [])
* @method static BaseModel|\Illuminate\Database\Eloquent\Collection|static|static[] findOrFail($id, $columns = [])
* @method static BaseModel|static findOrNew($id, $columns = [])
* @method static BaseModel|static firstOrNew($attributes = [], $values = [])
* @method static BaseModel|static firstOrCreate($attributes = [], $values = [])
* @method static BaseModel|static updateOrCreate($attributes, $values = [])
* @method static BaseModel|static firstOrFail($columns = [])
* @method static BaseModel|static|mixed firstOr($columns = [], $callback = null)
* @method static BaseModel sole($columns = [])
* @method static mixed value($column)
* @method static \Illuminate\Database\Eloquent\Collection[]|static[] get($columns = [])
* @method static BaseModel[]|static[] getModels($columns = [])
* @method static array eagerLoadRelations($models)
* @method static LazyCollection cursor()
* @method static Collection pluck($column, $key = null)
* @method static LengthAwarePaginator paginate($perPage = null, $columns = [], $pageName = 'page', $page = null)
* @method static Paginator simplePaginate($perPage = null, $columns = [], $pageName = 'page', $page = null)
* @method static CursorPaginator cursorPaginate($perPage = null, $columns = [], $cursorName = 'cursor', $cursor = null)
* @method static BaseModel|$this create($attributes = [])
* @method static BaseModel|$this forceCreate($attributes)
* @method static int upsert($values, $uniqueBy, $update = null)
* @method static void onDelete($callback)
* @method static static|mixed scopes($scopes)
* @method static static applyScopes()
* @method static \Illuminate\Database\Eloquent\Builder|static without($relations)
* @method static \Illuminate\Database\Eloquent\Builder|static withOnly($relations)
* @method static BaseModel newModelInstance($attributes = [])
* @method static \Illuminate\Database\Eloquent\Builder|static withCasts($casts)
* @method static Builder getQuery()
* @method static \Illuminate\Database\Eloquent\Builder|static setQuery($query)
* @method static Builder toBase()
* @method static array getEagerLoads()
* @method static \Illuminate\Database\Eloquent\Builder|static setEagerLoads($eagerLoad)
* @method static BaseModel getModel()
* @method static \Illuminate\Database\Eloquent\Builder|static setModel($model)
* @method static Closure getMacro($name)
* @method static bool hasMacro($name)
* @method static Closure getGlobalMacro($name)
* @method static bool hasGlobalMacro($name)
* @method static static clone ()
* @method static \Illuminate\Database\Eloquent\Builder|static has($relation, $operator = '>=', $count = 1, $boolean = 'and', $callback = null)
* @method static \Illuminate\Database\Eloquent\Builder|static orHas($relation, $operator = '>=', $count = 1)
* @method static \Illuminate\Database\Eloquent\Builder|static doesntHave($relation, $boolean = 'and', $callback = null)
* @method static \Illuminate\Database\Eloquent\Builder|static orDoesntHave($relation)
* @method static \Illuminate\Database\Eloquent\Builder|static whereHas($relation, $callback = null, $operator = '>=', $count = 1)
* @method static \Illuminate\Database\Eloquent\Builder|static orWhereHas($relation, $callback = null, $operator = '>=', $count = 1)
* @method static \Illuminate\Database\Eloquent\Builder|static whereDoesntHave($relation, $callback = null)
* @method static \Illuminate\Database\Eloquent\Builder|static orWhereDoesntHave($relation, $callback = null)
* @method static \Illuminate\Database\Eloquent\Builder|static hasMorph($relation, $types, $operator = '>=', $count = 1, $boolean = 'and', $callback = null)
* @method static \Illuminate\Database\Eloquent\Builder|static orHasMorph($relation, $types, $operator = '>=', $count = 1)
* @method static \Illuminate\Database\Eloquent\Builder|static doesntHaveMorph($relation, $types, $boolean = 'and', $callback = null)
* @method static \Illuminate\Database\Eloquent\Builder|static orDoesntHaveMorph($relation, $types)
* @method static \Illuminate\Database\Eloquent\Builder|static whereHasMorph($relation, $types, $callback = null, $operator = '>=', $count = 1)
* @method static \Illuminate\Database\Eloquent\Builder|static orWhereHasMorph($relation, $types, $callback = null, $operator = '>=', $count = 1)
* @method static \Illuminate\Database\Eloquent\Builder|static whereDoesntHaveMorph($relation, $types, $callback = null)
* @method static \Illuminate\Database\Eloquent\Builder|static orWhereDoesntHaveMorph($relation, $types, $callback = null)
* @method static \Illuminate\Database\Eloquent\Builder|static withAggregate($relations, $column, $function = null)
* @method static \Illuminate\Database\Eloquent\Builder|static withCount($relations)
* @method static \Illuminate\Database\Eloquent\Builder|static withMax($relation, $column)
* @method static \Illuminate\Database\Eloquent\Builder|static withMin($relation, $column)
* @method static \Illuminate\Database\Eloquent\Builder|static withSum($relation, $column)
* @method static \Illuminate\Database\Eloquent\Builder|static withAvg($relation, $column)
* @method static \Illuminate\Database\Eloquent\Builder|static withExists($relation)
* @method static \Illuminate\Database\Eloquent\Builder|static mergeConstraintsFrom($from)
* @method static Collection explain()
* @method static bool chunk($count, $callback)
* @method static Collection chunkMap($callback, $count = 1000)
* @method static bool each($callback, $count = 1000)
* @method static bool chunkById($count, $callback, $column = null, $alias = null)
* @method static bool eachById($callback, $count = 1000, $column = null, $alias = null)
* @method static LazyCollection lazy($chunkSize = 1000)
* @method static LazyCollection lazyById($chunkSize = 1000, $column = null, $alias = null)
* @method static BaseModel|object|static|null first($columns = [])
* @method static BaseModel|object|null baseSole($columns = [])
* @method static \Illuminate\Database\Eloquent\Builder|static tap($callback)
* @method static mixed when($value, $callback, $default = null)
* @method static mixed unless($value, $callback, $default = null)
* @method static Builder select($columns = [])
* @method static Builder selectSub($query, $as)
* @method static Builder selectRaw($expression, $bindings = [])
* @method static Builder fromSub($query, $as)
* @method static Builder fromRaw($expression, $bindings = [])
* @method static Builder addSelect($column)
* @method static Builder distinct()
* @method static Builder from($table, $as = null)
* @method static Builder join($table, $first, $operator = null, $second = null, $type = 'inner', $where = false)
* @method static Builder joinWhere($table, $first, $operator, $second, $type = 'inner')
* @method static Builder joinSub($query, $as, $first, $operator = null, $second = null, $type = 'inner', $where = false)
* @method static Builder leftJoin($table, $first, $operator = null, $second = null)
* @method static Builder leftJoinWhere($table, $first, $operator, $second)
* @method static Builder leftJoinSub($query, $as, $first, $operator = null, $second = null)
* @method static Builder rightJoin($table, $first, $operator = null, $second = null)
* @method static Builder rightJoinWhere($table, $first, $operator, $second)
* @method static Builder rightJoinSub($query, $as, $first, $operator = null, $second = null)
* @method static Builder crossJoin($table, $first = null, $operator = null, $second = null)
* @method static Builder crossJoinSub($query, $as)
* @method static void mergeWheres($wheres, $bindings)
* @method static array prepareValueAndOperator($value, $operator, $useDefault = false)
* @method static Builder whereColumn($first, $operator = null, $second = null, $boolean = 'and')
* @method static Builder orWhereColumn($first, $operator = null, $second = null)
* @method static Builder whereRaw($sql, $bindings = [], $boolean = 'and')
* @method static Builder orWhereRaw($sql, $bindings = [])
* @method static Builder whereIn($column, $values, $boolean = 'and', $not = false)
* @method static Builder orWhereIn($column, $values)
* @method static Builder whereNotIn($column, $values, $boolean = 'and')
* @method static Builder orWhereNotIn($column, $values)
* @method static Builder whereIntegerInRaw($column, $values, $boolean = 'and', $not = false)
* @method static Builder orWhereIntegerInRaw($column, $values)
* @method static Builder whereIntegerNotInRaw($column, $values, $boolean = 'and')
* @method static Builder orWhereIntegerNotInRaw($column, $values)
* @method static Builder whereNull($columns, $boolean = 'and', $not = false)
* @method static Builder orWhereNull($column)
* @method static Builder whereNotNull($columns, $boolean = 'and')
* @method static Builder whereBetween($column, $values, $boolean = 'and', $not = false)
* @method static Builder whereBetweenColumns($column, $values, $boolean = 'and', $not = false)
* @method static Builder orWhereBetween($column, $values)
* @method static Builder orWhereBetweenColumns($column, $values)
* @method static Builder whereNotBetween($column, $values, $boolean = 'and')
* @method static Builder whereNotBetweenColumns($column, $values, $boolean = 'and')
* @method static Builder orWhereNotBetween($column, $values)
* @method static Builder orWhereNotBetweenColumns($column, $values)
* @method static Builder orWhereNotNull($column)
* @method static Builder whereDate($column, $operator, $value = null, $boolean = 'and')
* @method static Builder orWhereDate($column, $operator, $value = null)
* @method static Builder whereTime($column, $operator, $value = null, $boolean = 'and')
* @method static Builder orWhereTime($column, $operator, $value = null)
* @method static Builder whereDay($column, $operator, $value = null, $boolean = 'and')
* @method static Builder orWhereDay($column, $operator, $value = null)
* @method static Builder whereMonth($column, $operator, $value = null, $boolean = 'and')
* @method static Builder orWhereMonth($column, $operator, $value = null)
* @method static Builder whereYear($column, $operator, $value = null, $boolean = 'and')
* @method static Builder orWhereYear($column, $operator, $value = null)
* @method static Builder whereNested($callback, $boolean = 'and')
* @method static Builder forNestedWhere()
* @method static Builder addNestedWhereQuery($query, $boolean = 'and')
* @method static Builder whereExists($callback, $boolean = 'and', $not = false)
* @method static Builder orWhereExists($callback, $not = false)
* @method static Builder whereNotExists($callback, $boolean = 'and')
* @method static Builder orWhereNotExists($callback)
* @method static Builder addWhereExistsQuery($query, $boolean = 'and', $not = false)
* @method static Builder whereRowValues($columns, $operator, $values, $boolean = 'and')
* @method static Builder orWhereRowValues($columns, $operator, $values)
* @method static Builder whereJsonContains($column, $value, $boolean = 'and', $not = false)
* @method static Builder orWhereJsonContains($column, $value)
* @method static Builder whereJsonDoesntContain($column, $value, $boolean = 'and')
* @method static Builder orWhereJsonDoesntContain($column, $value)
* @method static Builder whereJsonLength($column, $operator, $value = null, $boolean = 'and')
* @method static Builder orWhereJsonLength($column, $operator, $value = null)
* @method static Builder dynamicWhere($method, $parameters)
* @method static Builder groupBy(...$groups)
* @method static Builder groupByRaw($sql, $bindings = [])
* @method static Builder having($column, $operator = null, $value = null, $boolean = 'and')
* @method static Builder orHaving($column, $operator = null, $value = null)
* @method static Builder havingBetween($column, $values, $boolean = 'and', $not = false)
* @method static Builder havingRaw($sql, $bindings = [], $boolean = 'and')
* @method static Builder orHavingRaw($sql, $bindings = [])
* @method static Builder orderBy($column, $direction = 'asc')
* @method static Builder orderByDesc($column)
* @method static Builder inRandomOrder($seed = '')
* @method static Builder orderByRaw($sql, $bindings = [])
* @method static Builder skip($value)
* @method static Builder offset($value)
* @method static Builder take($value)
* @method static Builder limit($value)
* @method static Builder forPage($page, $perPage = 15)
* @method static Builder forPageBeforeId($perPage = 15, $lastId = 0, $column = 'id')
* @method static Builder forPageAfterId($perPage = 15, $lastId = 0, $column = 'id')
* @method static Builder reorder($column = null, $direction = 'asc')
* @method static Builder union($query, $all = false)
* @method static Builder unionAll($query)
* @method static Builder lock($value = true)
* @method static Builder lockForUpdate()
* @method static Builder sharedLock()
* @method static Builder beforeQuery($callback)
* @method static void applyBeforeQueryCallbacks()
* @method static string toSql()
* @method static int getCountForPagination($columns = [])
* @method static string implode($column, $glue = '')
* @method static bool exists()
* @method static bool doesntExist()
* @method static mixed existsOr($callback)
* @method static mixed doesntExistOr($callback)
* @method static int count($columns = '*')
* @method static mixed min($column)
* @method static mixed max($column)
* @method static mixed sum($column)
* @method static mixed avg($column)
* @method static mixed average($column)
* @method static mixed aggregate($function, $columns = [])
* @method static float|int numericAggregate($function, $columns = [])
* @method static bool insert($values)
* @method static int insertOrIgnore($values)
* @method static int insertGetId($values, $sequence = null)
* @method static int insertUsing($columns, $query)
* @method static bool updateOrInsert($attributes, $values = [])
* @method static void truncate()
* @method static Expression raw($value)
* @method static array getBindings()
* @method static array getRawBindings()
* @method static Builder setBindings($bindings, $type = 'where')
* @method static Builder addBinding($value, $type = 'where')
* @method static Builder mergeBindings($query)
* @method static array cleanBindings($bindings)
* @method static Processor getProcessor()
* @method static Grammar getGrammar()
* @method static Builder useWritePdo()
* @method static static cloneWithout($properties)
* @method static static cloneWithoutBindings($except)
* @method static Builder dump()
* @method static void dd()
* @method static void macro($name, $macro)
* @method static void mixin($mixin, $replace = true)
* @method static mixed macroCall($method, $parameters)
*/
class Model extends BaseModel
{
}
+264
View File
@@ -0,0 +1,264 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace support;
use Closure;
use Illuminate\Contracts\Pagination\CursorPaginator;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Contracts\Pagination\Paginator;
use MongoDB\Laravel\Eloquent\Model as BaseModel;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\Expression;
use Illuminate\Database\Query\Grammars\Grammar;
use Illuminate\Database\Query\Processors\Processor;
use Illuminate\Support\Collection;
use Illuminate\Support\LazyCollection;
require_once __DIR__ . '/../Initializer.php';
/**
* @method static \Illuminate\Database\Eloquent\Model make($attributes = [])
* @method static \Illuminate\Database\Eloquent\Builder|static withGlobalScope($identifier, $scope)
* @method static \Illuminate\Database\Eloquent\Builder|static withoutGlobalScope($scope)
* @method static \Illuminate\Database\Eloquent\Builder|static withoutGlobalScopes($scopes = null)
* @method static array removedScopes()
* @method static \Illuminate\Database\Eloquent\Builder|static whereKey($id)
* @method static \Illuminate\Database\Eloquent\Builder|static whereKeyNot($id)
* @method static \Illuminate\Database\Eloquent\Builder|static where($column, $operator = null, $value = null, $boolean = 'and')
* @method static BaseModel|null firstWhere($column, $operator = null, $value = null, $boolean = 'and')
* @method static \Illuminate\Database\Eloquent\Builder|static orWhere($column, $operator = null, $value = null)
* @method static \Illuminate\Database\Eloquent\Builder|static latest($column = null)
* @method static \Illuminate\Database\Eloquent\Builder|static oldest($column = null)
* @method static \Illuminate\Database\Eloquent\Collection|static hydrate($items)
* @method static \Illuminate\Database\Eloquent\Collection|static fromQuery($query, $bindings = [])
* @method static BaseModel|\Illuminate\Database\Eloquent\Collection|static[]|static|null find($id, $columns = [])
* @method static \Illuminate\Database\Eloquent\Collection|static findMany($ids, $columns = [])
* @method static BaseModel|\Illuminate\Database\Eloquent\Collection|static|static[] findOrFail($id, $columns = [])
* @method static BaseModel|static findOrNew($id, $columns = [])
* @method static BaseModel|static firstOrNew($attributes = [], $values = [])
* @method static BaseModel|static firstOrCreate($attributes = [], $values = [])
* @method static BaseModel|static updateOrCreate($attributes, $values = [])
* @method static BaseModel|static firstOrFail($columns = [])
* @method static BaseModel|static|mixed firstOr($columns = [], $callback = null)
* @method static BaseModel sole($columns = [])
* @method static mixed value($column)
* @method static \Illuminate\Database\Eloquent\Collection[]|static[] get($columns = [])
* @method static BaseModel[]|static[] getModels($columns = [])
* @method static array eagerLoadRelations($models)
* @method static LazyCollection cursor()
* @method static Collection pluck($column, $key = null)
* @method static LengthAwarePaginator paginate($perPage = null, $columns = [], $pageName = 'page', $page = null)
* @method static Paginator simplePaginate($perPage = null, $columns = [], $pageName = 'page', $page = null)
* @method static CursorPaginator cursorPaginate($perPage = null, $columns = [], $cursorName = 'cursor', $cursor = null)
* @method static BaseModel|$this create($attributes = [])
* @method static BaseModel|$this forceCreate($attributes)
* @method static int upsert($values, $uniqueBy, $update = null)
* @method static void onDelete($callback)
* @method static static|mixed scopes($scopes)
* @method static static applyScopes()
* @method static \Illuminate\Database\Eloquent\Builder|static without($relations)
* @method static \Illuminate\Database\Eloquent\Builder|static withOnly($relations)
* @method static BaseModel newModelInstance($attributes = [])
* @method static \Illuminate\Database\Eloquent\Builder|static withCasts($casts)
* @method static Builder getQuery()
* @method static \Illuminate\Database\Eloquent\Builder|static setQuery($query)
* @method static Builder toBase()
* @method static array getEagerLoads()
* @method static \Illuminate\Database\Eloquent\Builder|static setEagerLoads($eagerLoad)
* @method static BaseModel getModel()
* @method static \Illuminate\Database\Eloquent\Builder|static setModel($model)
* @method static Closure getMacro($name)
* @method static bool hasMacro($name)
* @method static Closure getGlobalMacro($name)
* @method static bool hasGlobalMacro($name)
* @method static static clone ()
* @method static \Illuminate\Database\Eloquent\Builder|static has($relation, $operator = '>=', $count = 1, $boolean = 'and', $callback = null)
* @method static \Illuminate\Database\Eloquent\Builder|static orHas($relation, $operator = '>=', $count = 1)
* @method static \Illuminate\Database\Eloquent\Builder|static doesntHave($relation, $boolean = 'and', $callback = null)
* @method static \Illuminate\Database\Eloquent\Builder|static orDoesntHave($relation)
* @method static \Illuminate\Database\Eloquent\Builder|static whereHas($relation, $callback = null, $operator = '>=', $count = 1)
* @method static \Illuminate\Database\Eloquent\Builder|static orWhereHas($relation, $callback = null, $operator = '>=', $count = 1)
* @method static \Illuminate\Database\Eloquent\Builder|static whereDoesntHave($relation, $callback = null)
* @method static \Illuminate\Database\Eloquent\Builder|static orWhereDoesntHave($relation, $callback = null)
* @method static \Illuminate\Database\Eloquent\Builder|static hasMorph($relation, $types, $operator = '>=', $count = 1, $boolean = 'and', $callback = null)
* @method static \Illuminate\Database\Eloquent\Builder|static orHasMorph($relation, $types, $operator = '>=', $count = 1)
* @method static \Illuminate\Database\Eloquent\Builder|static doesntHaveMorph($relation, $types, $boolean = 'and', $callback = null)
* @method static \Illuminate\Database\Eloquent\Builder|static orDoesntHaveMorph($relation, $types)
* @method static \Illuminate\Database\Eloquent\Builder|static whereHasMorph($relation, $types, $callback = null, $operator = '>=', $count = 1)
* @method static \Illuminate\Database\Eloquent\Builder|static orWhereHasMorph($relation, $types, $callback = null, $operator = '>=', $count = 1)
* @method static \Illuminate\Database\Eloquent\Builder|static whereDoesntHaveMorph($relation, $types, $callback = null)
* @method static \Illuminate\Database\Eloquent\Builder|static orWhereDoesntHaveMorph($relation, $types, $callback = null)
* @method static \Illuminate\Database\Eloquent\Builder|static withAggregate($relations, $column, $function = null)
* @method static \Illuminate\Database\Eloquent\Builder|static withCount($relations)
* @method static \Illuminate\Database\Eloquent\Builder|static withMax($relation, $column)
* @method static \Illuminate\Database\Eloquent\Builder|static withMin($relation, $column)
* @method static \Illuminate\Database\Eloquent\Builder|static withSum($relation, $column)
* @method static \Illuminate\Database\Eloquent\Builder|static withAvg($relation, $column)
* @method static \Illuminate\Database\Eloquent\Builder|static withExists($relation)
* @method static \Illuminate\Database\Eloquent\Builder|static mergeConstraintsFrom($from)
* @method static Collection explain()
* @method static bool chunk($count, $callback)
* @method static Collection chunkMap($callback, $count = 1000)
* @method static bool each($callback, $count = 1000)
* @method static bool chunkById($count, $callback, $column = null, $alias = null)
* @method static bool eachById($callback, $count = 1000, $column = null, $alias = null)
* @method static LazyCollection lazy($chunkSize = 1000)
* @method static LazyCollection lazyById($chunkSize = 1000, $column = null, $alias = null)
* @method static BaseModel|object|static|null first($columns = [])
* @method static BaseModel|object|null baseSole($columns = [])
* @method static \Illuminate\Database\Eloquent\Builder|static tap($callback)
* @method static mixed when($value, $callback, $default = null)
* @method static mixed unless($value, $callback, $default = null)
* @method static Builder select($columns = [])
* @method static Builder selectSub($query, $as)
* @method static Builder selectRaw($expression, $bindings = [])
* @method static Builder fromSub($query, $as)
* @method static Builder fromRaw($expression, $bindings = [])
* @method static Builder addSelect($column)
* @method static Builder distinct()
* @method static Builder from($table, $as = null)
* @method static Builder join($table, $first, $operator = null, $second = null, $type = 'inner', $where = false)
* @method static Builder joinWhere($table, $first, $operator, $second, $type = 'inner')
* @method static Builder joinSub($query, $as, $first, $operator = null, $second = null, $type = 'inner', $where = false)
* @method static Builder leftJoin($table, $first, $operator = null, $second = null)
* @method static Builder leftJoinWhere($table, $first, $operator, $second)
* @method static Builder leftJoinSub($query, $as, $first, $operator = null, $second = null)
* @method static Builder rightJoin($table, $first, $operator = null, $second = null)
* @method static Builder rightJoinWhere($table, $first, $operator, $second)
* @method static Builder rightJoinSub($query, $as, $first, $operator = null, $second = null)
* @method static Builder crossJoin($table, $first = null, $operator = null, $second = null)
* @method static Builder crossJoinSub($query, $as)
* @method static void mergeWheres($wheres, $bindings)
* @method static array prepareValueAndOperator($value, $operator, $useDefault = false)
* @method static Builder whereColumn($first, $operator = null, $second = null, $boolean = 'and')
* @method static Builder orWhereColumn($first, $operator = null, $second = null)
* @method static Builder whereRaw($sql, $bindings = [], $boolean = 'and')
* @method static Builder orWhereRaw($sql, $bindings = [])
* @method static Builder whereIn($column, $values, $boolean = 'and', $not = false)
* @method static Builder orWhereIn($column, $values)
* @method static Builder whereNotIn($column, $values, $boolean = 'and')
* @method static Builder orWhereNotIn($column, $values)
* @method static Builder whereIntegerInRaw($column, $values, $boolean = 'and', $not = false)
* @method static Builder orWhereIntegerInRaw($column, $values)
* @method static Builder whereIntegerNotInRaw($column, $values, $boolean = 'and')
* @method static Builder orWhereIntegerNotInRaw($column, $values)
* @method static Builder whereNull($columns, $boolean = 'and', $not = false)
* @method static Builder orWhereNull($column)
* @method static Builder whereNotNull($columns, $boolean = 'and')
* @method static Builder whereBetween($column, $values, $boolean = 'and', $not = false)
* @method static Builder whereBetweenColumns($column, $values, $boolean = 'and', $not = false)
* @method static Builder orWhereBetween($column, $values)
* @method static Builder orWhereBetweenColumns($column, $values)
* @method static Builder whereNotBetween($column, $values, $boolean = 'and')
* @method static Builder whereNotBetweenColumns($column, $values, $boolean = 'and')
* @method static Builder orWhereNotBetween($column, $values)
* @method static Builder orWhereNotBetweenColumns($column, $values)
* @method static Builder orWhereNotNull($column)
* @method static Builder whereDate($column, $operator, $value = null, $boolean = 'and')
* @method static Builder orWhereDate($column, $operator, $value = null)
* @method static Builder whereTime($column, $operator, $value = null, $boolean = 'and')
* @method static Builder orWhereTime($column, $operator, $value = null)
* @method static Builder whereDay($column, $operator, $value = null, $boolean = 'and')
* @method static Builder orWhereDay($column, $operator, $value = null)
* @method static Builder whereMonth($column, $operator, $value = null, $boolean = 'and')
* @method static Builder orWhereMonth($column, $operator, $value = null)
* @method static Builder whereYear($column, $operator, $value = null, $boolean = 'and')
* @method static Builder orWhereYear($column, $operator, $value = null)
* @method static Builder whereNested($callback, $boolean = 'and')
* @method static Builder forNestedWhere()
* @method static Builder addNestedWhereQuery($query, $boolean = 'and')
* @method static Builder whereExists($callback, $boolean = 'and', $not = false)
* @method static Builder orWhereExists($callback, $not = false)
* @method static Builder whereNotExists($callback, $boolean = 'and')
* @method static Builder orWhereNotExists($callback)
* @method static Builder addWhereExistsQuery($query, $boolean = 'and', $not = false)
* @method static Builder whereRowValues($columns, $operator, $values, $boolean = 'and')
* @method static Builder orWhereRowValues($columns, $operator, $values)
* @method static Builder whereJsonContains($column, $value, $boolean = 'and', $not = false)
* @method static Builder orWhereJsonContains($column, $value)
* @method static Builder whereJsonDoesntContain($column, $value, $boolean = 'and')
* @method static Builder orWhereJsonDoesntContain($column, $value)
* @method static Builder whereJsonLength($column, $operator, $value = null, $boolean = 'and')
* @method static Builder orWhereJsonLength($column, $operator, $value = null)
* @method static Builder dynamicWhere($method, $parameters)
* @method static Builder groupBy(...$groups)
* @method static Builder groupByRaw($sql, $bindings = [])
* @method static Builder having($column, $operator = null, $value = null, $boolean = 'and')
* @method static Builder orHaving($column, $operator = null, $value = null)
* @method static Builder havingBetween($column, $values, $boolean = 'and', $not = false)
* @method static Builder havingRaw($sql, $bindings = [], $boolean = 'and')
* @method static Builder orHavingRaw($sql, $bindings = [])
* @method static Builder orderBy($column, $direction = 'asc')
* @method static Builder orderByDesc($column)
* @method static Builder inRandomOrder($seed = '')
* @method static Builder orderByRaw($sql, $bindings = [])
* @method static Builder skip($value)
* @method static Builder offset($value)
* @method static Builder take($value)
* @method static Builder limit($value)
* @method static Builder forPage($page, $perPage = 15)
* @method static Builder forPageBeforeId($perPage = 15, $lastId = 0, $column = 'id')
* @method static Builder forPageAfterId($perPage = 15, $lastId = 0, $column = 'id')
* @method static Builder reorder($column = null, $direction = 'asc')
* @method static Builder union($query, $all = false)
* @method static Builder unionAll($query)
* @method static Builder lock($value = true)
* @method static Builder lockForUpdate()
* @method static Builder sharedLock()
* @method static Builder beforeQuery($callback)
* @method static void applyBeforeQueryCallbacks()
* @method static string toSql()
* @method static int getCountForPagination($columns = [])
* @method static string implode($column, $glue = '')
* @method static bool exists()
* @method static bool doesntExist()
* @method static mixed existsOr($callback)
* @method static mixed doesntExistOr($callback)
* @method static int count($columns = '*')
* @method static mixed min($column)
* @method static mixed max($column)
* @method static mixed sum($column)
* @method static mixed avg($column)
* @method static mixed average($column)
* @method static mixed aggregate($function, $columns = [])
* @method static float|int numericAggregate($function, $columns = [])
* @method static bool insert($values)
* @method static int insertOrIgnore($values)
* @method static int insertGetId($values, $sequence = null)
* @method static int insertUsing($columns, $query)
* @method static bool updateOrInsert($attributes, $values = [])
* @method static void truncate()
* @method static Expression raw($value)
* @method static array getBindings()
* @method static array getRawBindings()
* @method static Builder setBindings($bindings, $type = 'where')
* @method static Builder addBinding($value, $type = 'where')
* @method static Builder mergeBindings($query)
* @method static array cleanBindings($bindings)
* @method static Processor getProcessor()
* @method static Grammar getGrammar()
* @method static Builder useWritePdo()
* @method static static cloneWithout($properties)
* @method static static cloneWithoutBindings($except)
* @method static Builder dump()
* @method static void dd()
* @method static void macro($name, $macro)
* @method static void mixin($mixin, $replace = true)
* @method static mixed macroCall($method, $parameters)
*/
class MongoModel extends BaseModel
{
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 webman
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.
+2
View File
@@ -0,0 +1,2 @@
# Webman redis
This is Webman's redis library, which includes redis client and connection pool.
+18
View File
@@ -0,0 +1,18 @@
{
"name": "webman/redis",
"type": "library",
"license": "MIT",
"description": "Webman redis",
"require": {
"workerman/webman-framework": "^2.1 || dev-master",
"illuminate/redis": "^10.0 || ^11.0 || ^12.0 || ^13.0"
},
"autoload": {
"psr-4": {
"Webman\\Redis\\": "src",
"support\\": "src/support"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}
+73
View File
@@ -0,0 +1,73 @@
<?php
namespace Webman\Redis;
class Install
{
const WEBMAN_PLUGIN = true;
/**
* @var array
*/
protected static array $pathRelation = [];
/**
* Install
* @return void
*/
public static function install(): void
{
$redis_file = config_path('redis.php');
if (!is_file($redis_file)) {
echo 'Create config/redis.php' . PHP_EOL;
copy(__DIR__ . '/config/redis.php', $redis_file);
}
static::installByRelation();
}
/**
* Uninstall
* @return void
*/
public static function uninstall(): void
{
$redis_file = config_path('redis.php');
if (is_file($redis_file)) {
echo 'Remove config/redis.php' . PHP_EOL;
unlink($redis_file);
}
self::uninstallByRelation();
}
/**
* installByRelation
* @return void
*/
public static function installByRelation(): void
{
foreach (static::$pathRelation as $source => $dest) {
if ($pos = strrpos($dest, '/')) {
$parent_dir = base_path().'/'.substr($dest, 0, $pos);
if (!is_dir($parent_dir)) {
mkdir($parent_dir, 0777, true);
}
}
copy_dir(__DIR__ . "/$source", base_path()."/$dest");
}
}
/**
* uninstallByRelation
* @return void
*/
public static function uninstallByRelation(): void
{
foreach (static::$pathRelation as $source => $dest) {
$path = base_path()."/$dest";
if (!is_dir($path) && !is_file($path)) {
continue;
}
remove_dir($path);
}
}
}
+90
View File
@@ -0,0 +1,90 @@
<?php
namespace Webman\Redis;
use Illuminate\Events\Dispatcher;
use Illuminate\Redis\Connections\Connection;
use StdClass;
use Throwable;
use WeakMap;
use Webman\Context;
use Workerman\Coroutine\Pool;
class RedisManager extends \Illuminate\Redis\RedisManager
{
/**
* @var Pool[]
*/
protected static array $pools = [];
/**
* @var WeakMap
*/
protected WeakMap $allConnections;
/**
* Get connection.
*
* @param $name
* @return Connection|mixed|StdClass|null
* @throws Throwable
*/
public function connection($name = null)
{
$name = $name ?: 'default';
$key = "redis.connections.$name";
$connection = Context::get($key);
if (!$connection) {
if (!isset(static::$pools[$name])) {
$poolConfig = $this->config[$name]['pool'] ?? [];
$pool = new Pool($poolConfig['max_connections'] ?? 10, $poolConfig);
$pool->setConnectionCreator(function () use ($name) {
$connection = $this->configure($this->resolve($name), $name);
if (class_exists(Dispatcher::class)) {
$connection->setEventDispatcher(new Dispatcher());
}
$this->allConnections ??= new WeakMap();
$this->allConnections[$connection] = true;
return $connection;
});
$pool->setConnectionCloser(function ($connection) {
$connection->client()->close();
});
$pool->setHeartbeatChecker(function ($connection) {
$connection->get('PING');
});
static::$pools[$name] = $pool;
}
try {
$connection = static::$pools[$name]->get();
Context::set($key, $connection);
} finally {
Context::onDestroy(function () use ($connection, $name) {
try {
$connection && static::$pools[$name]->put($connection);
} catch (Throwable) {
// ignore
}
});
}
}
return $connection;
}
/**
* Return all the created connections.
*
* @return array
*/
public function connections()
{
if (empty($this->allConnections)) {
return [];
}
$connections = [];
foreach ($this->allConnections as $connection => $_) {
$connections[] = $connection;
}
return $connections;
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return [
'default' => [
'password' => '',
'host' => '127.0.0.1',
'port' => 6379,
'database' => 0,
'pool' => [
'max_connections' => 5,
'min_connections' => 1,
'wait_timeout' => 3,
'idle_timeout' => 60,
'heartbeat_interval' => 50,
],
]
];
+284
View File
@@ -0,0 +1,284 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace support;
use Illuminate\Redis\Connections\Connection;
use Throwable;
use Webman\Redis\RedisManager;
use Redis as RedisClient;
use function config;
use function in_array;
/**
* Class Redis
* @package support
*
* Strings methods
* @method static int append($key, $value)
* @method static int bitCount($key)
* @method static int decr($key, $value = 1)
* @method static int decrBy($key, $value)
* @method static string|bool get($key)
* @method static int getBit($key, $offset)
* @method static string getRange($key, $start, $end)
* @method static string getSet($key, $value)
* @method static int incr($key, $value = 1)
* @method static int incrBy($key, $value)
* @method static float incrByFloat($key, $value)
* @method static array mGet(array $keys)
* @method static array getMultiple(array $keys)
* @method static bool mSet($pairs)
* @method static bool mSetNx($pairs)
* @method static bool set($key, $value, $expireResolution = null, $expireTTL = null, $flag = null)
* @method static bool setBit($key, $offset, $value)
* @method static bool setEx($key, $ttl, $value)
* @method static bool pSetEx($key, $ttl, $value)
* @method static bool setNx($key, $value)
* @method static string setRange($key, $offset, $value)
* @method static int strLen($key)
* Keys methods
* @method static int del(...$keys)
* @method static int unlink(...$keys)
* @method static false|string dump($key)
* @method static int exists(...$keys)
* @method static bool expire($key, $ttl)
* @method static bool pexpire($key, $ttl)
* @method static bool expireAt($key, $timestamp)
* @method static bool pexpireAt($key, $timestamp)
* @method static array keys($pattern)
* @method static bool|array scan($it)
* @method static void migrate($host, $port, $keys, $dbIndex, $timeout, $copy = false, $replace = false)
* @method static bool select($dbIndex)
* @method static bool move($key, $dbIndex)
* @method static string|int|bool object($information, $key)
* @method static bool persist($key)
* @method static string randomKey()
* @method static bool rename($srcKey, $dstKey)
* @method static bool renameNx($srcKey, $dstKey)
* @method static string type($key)
* @method static int|array sort($key, $options = [])
* @method static int ttl($key)
* @method static int pttl($key)
* @method static void restore($key, $ttl, $value)
* Hashes methods
* @method static false|int hSet($key, $hashKey, $value)
* @method static bool hSetNx($key, $hashKey, $value)
* @method static false|string hGet($key, $hashKey)
* @method static false|int hLen($key)
* @method static false|int hDel($key, ...$hashKeys)
* @method static array hKeys($key)
* @method static array hVals($key)
* @method static array hGetAll($key)
* @method static bool hExists($key, $hashKey)
* @method static int hIncrBy($key, $hashKey, $value)
* @method static float hIncrByFloat($key, $hashKey, $value)
* @method static bool hMSet($key, $members)
* @method static array hMGet($key, $memberKeys)
* @method static array hScan($key, $iterator, $pattern = '', $count = 0)
* @method static int hStrLen($key, $hashKey)
* Lists methods
* @method static array blPop($keys, $timeout)
* @method static array brPop($keys, $timeout)
* @method static false|string bRPopLPush($srcKey, $dstKey, $timeout)
* @method static false|string lIndex($key, $index)
* @method static int lInsert($key, $position, $pivot, $value)
* @method static false|string lPop($key)
* @method static false|int lPush($key, ...$entries)
* @method static false|int lPushx($key, $value)
* @method static array lRange($key, $start, $end)
* @method static false|int lRem($key, $count, $value)
* @method static bool lSet($key, $index, $value)
* @method static false|array lTrim($key, $start, $end)
* @method static false|string rPop($key)
* @method static false|string rPopLPush($srcKey, $dstKey)
* @method static false|int rPush($key, ...$entries)
* @method static false|int rPushX($key, $value)
* @method static false|int lLen($key)
* Sets methods
* @method static int sAdd($key, $value)
* @method static int sCard($key)
* @method static array sDiff($keys)
* @method static false|int sDiffStore($dst, $keys)
* @method static false|array sInter($keys)
* @method static false|int sInterStore($dst, $keys)
* @method static bool sIsMember($key, $member)
* @method static array sMembers($key)
* @method static bool sMove($src, $dst, $member)
* @method static false|string|array sPop($key, $count = 0)
* @method static false|string|array sRandMember($key, $count = 0)
* @method static int sRem($key, ...$members)
* @method static array sUnion(...$keys)
* @method static false|int sUnionStore($dst, ...$keys)
* @method static false|array sScan($key, $iterator, $pattern = '', $count = 0)
* Sorted sets methods
* @method static array bzPopMin($keys, $timeout)
* @method static array bzPopMax($keys, $timeout)
* @method static int zAdd($key, $score, $value)
* @method static int zCard($key)
* @method static int zCount($key, $start, $end)
* @method static double zIncrBy($key, $value, $member)
* @method static int zinterstore($keyOutput, $arrayZSetKeys, $arrayWeights = [], $aggregateFunction = '')
* @method static array zPopMin($key, $count)
* @method static array zPopMax($key, $count)
* @method static array zRange($key, $start, $end, $withScores = false)
* @method static array zRangeByScore($key, $start, $end, $options = [])
* @method static array zRevRangeByScore($key, $start, $end, $options = [])
* @method static array zRangeByLex($key, $min, $max, $offset = 0, $limit = 0)
* @method static int zRank($key, $member)
* @method static int zRevRank($key, $member)
* @method static int zRem($key, ...$members)
* @method static int zRemRangeByRank($key, $start, $end)
* @method static int zRemRangeByScore($key, $start, $end)
* @method static array zRevRange($key, $start, $end, $withScores = false)
* @method static double zScore($key, $member)
* @method static int zunionstore($keyOutput, $arrayZSetKeys, $arrayWeights = [], $aggregateFunction = '')
* @method static false|array zScan($key, $iterator, $pattern = '', $count = 0)
* HyperLogLogs methods
* @method static int pfAdd($key, $values)
* @method static int pfCount($keys)
* @method static bool pfMerge($dstKey, $srcKeys)
* Geocoding methods
* @method static int geoAdd($key, $longitude, $latitude, $member, ...$items)
* @method static array geoHash($key, ...$members)
* @method static array geoPos($key, ...$members)
* @method static double geoDist($key, $members, $unit = '')
* @method static int|array geoRadius($key, $longitude, $latitude, $radius, $unit, $options = [])
* @method static array geoRadiusByMember($key, $member, $radius, $units, $options = [])
* Streams methods
* @method static int xAck($stream, $group, $arrMessages)
* @method static string xAdd($strKey, $strId, $arrMessage, $iMaxLen = 0, $booApproximate = false)
* @method static array xClaim($strKey, $strGroup, $strConsumer, $minIdleTime, $arrIds, $arrOptions = [])
* @method static int xDel($strKey, $arrIds)
* @method static mixed xGroup($command, $strKey, $strGroup, $strMsgId, $booMKStream = null)
* @method static mixed xInfo($command, $strStream, $strGroup = null)
* @method static int xLen($stream)
* @method static array xPending($strStream, $strGroup, $strStart = 0, $strEnd = 0, $iCount = 0, $strConsumer = null)
* @method static array xRange($strStream, $strStart, $strEnd, $iCount = 0)
* @method static array xRead($arrStreams, $iCount = 0, $iBlock = null)
* @method static array xReadGroup($strGroup, $strConsumer, $arrStreams, $iCount = 0, $iBlock = null)
* @method static array xRevRange($strStream, $strEnd, $strStart, $iCount = 0)
* @method static int xTrim($strStream, $iMaxLen, $booApproximate = null)
* Pub/sub methods
* @method static mixed pSubscribe($patterns, $callback)
* @method static mixed publish($channel, $message)
* @method static mixed subscribe($channels, $callback)
* @method static mixed pubSub($keyword, $argument = null)
* Generic methods
* @method static mixed rawCommand(...$commandAndArgs)
* Pipeline methods
* @method static RedisClient|array pipeline(callable|null $callback = null)
* Transactions methods
* @method static RedisClient multi()
* @method static mixed exec()
* @method static mixed discard()
* @method static mixed watch($keys)
* @method static mixed unwatch($keys)
* Scripting methods
* @method static mixed eval($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null)
* @method static mixed evalSha($scriptSha, $numkeys, ...$arguments)
* @method static mixed script($command, ...$scripts)
* @method static mixed client(...$args)
* @method static null|string getLastError()
* @method static bool clearLastError()
* @method static mixed _prefix($value)
* @method static mixed _serialize($value)
* @method static mixed _unserialize($value)
* Introspection methods
* @method static bool isConnected()
* @method static mixed getHost()
* @method static mixed getPort()
* @method static false|int getDbNum()
* @method static false|double getTimeout()
* @method static mixed getReadTimeout()
* @method static mixed getPersistentID()
* @method static mixed getAuth()
*/
class Redis
{
/**
* @var RedisManager|null
*/
protected static ?RedisManager $instance = null;
/**
* need to install phpredis extension
*/
const PHPREDIS_CLIENT = 'phpredis';
/**
* need to install the 'predis/predis' packgage.
* cmd: composer install predis/predis
*/
const PREDIS_CLIENT = 'predis';
/**
* Support client collection
*/
static array $allowClient = [
self::PHPREDIS_CLIENT,
self::PREDIS_CLIENT
];
/**
* The Redis server configurations.
*
* @var array
*/
protected static array $config = [];
/**
* @return RedisManager|null
*/
public static function instance(): ?RedisManager
{
if (!static::$instance) {
$config = config('redis');
$client = $config['client'] ?? self::PHPREDIS_CLIENT;
if (!in_array($client, static::$allowClient)) {
$client = self::PHPREDIS_CLIENT;
}
static::$config = $config;
static::$instance = new RedisManager('', $client, $config);
}
return static::$instance;
}
/**
* Connection.
* @param string $name
* @return Connection|RedisClient
* @throws Throwable
*/
public static function connection(string $name = 'default'): Connection
{
return static::instance()->connection($name);
}
/**
* @param string $name
* @param array $arguments
* @return mixed
* @throws Throwable
*/
public static function __callStatic(string $name, array $arguments)
{
return static::connection()->{$name}(... $arguments);
}
}
+2148
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
{
"name": "webman/validation",
"type": "library",
"license": "MIT",
"description": "Webman plugin webman/validation",
"require": {
"illuminate/validation": "*",
"illuminate/translation": "*",
"workerman/webman-framework": "^2.1 || dev-master",
"webman/console": ">=2.1.7"
},
"autoload": {
"psr-4": {
"Webman\\Validation\\": "src",
"support\\validation\\": "src/support/validation"
}
},
"autoload-dev": {
"psr-4": {
"Webman\\Validation\\Tests\\": "src/Tests"
}
},
"require-dev": {
"phpunit/phpunit": "*"
}
}
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="src/Tests/bootstrap.php"
colors="true"
cacheDirectory=".phpunit.cache">
<testsuites>
<testsuite name="webman-validation">
<directory>src/Tests</directory>
</testsuite>
</testsuites>
</phpunit>
@@ -0,0 +1,170 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Validierungsmeldungen
|--------------------------------------------------------------------------
*/
'accepted' => 'Das Feld :attribute muss akzeptiert werden.',
'accepted_if' => 'Das Feld :attribute muss akzeptiert werden, wenn :other :value ist.',
'active_url' => 'Das Feld :attribute muss eine gültige URL sein.',
'after' => 'Das Feld :attribute muss ein Datum nach dem :date sein.',
'after_or_equal' => 'Das Feld :attribute muss ein Datum nach oder gleich :date sein.',
'alpha' => 'Das Feld :attribute darf nur Buchstaben enthalten.',
'alpha_dash' => 'Das Feld :attribute darf nur Buchstaben, Zahlen, Bindestriche und Unterstriche enthalten.',
'alpha_num' => 'Das Feld :attribute darf nur Buchstaben und Zahlen enthalten.',
'any_of' => 'Das Feld :attribute ist ungültig.',
'array' => 'Das Feld :attribute muss ein Array sein.',
'ascii' => 'Das Feld :attribute darf nur einbyte alphanumerische Zeichen und Symbole enthalten.',
'before' => 'Das Feld :attribute muss ein Datum vor dem :date sein.',
'before_or_equal' => 'Das Feld :attribute muss ein Datum vor oder gleich :date sein.',
'between' => [
'array' => 'Das Feld :attribute muss zwischen :min und :max Elemente haben.',
'file' => 'Das Feld :attribute muss zwischen :min und :max Kilobyte sein.',
'numeric' => 'Das Feld :attribute muss zwischen :min und :max liegen.',
'string' => 'Das Feld :attribute muss zwischen :min und :max Zeichen haben.',
],
'boolean' => 'Das Feld :attribute muss wahr oder falsch sein.',
'can' => 'Das Feld :attribute enthält einen nicht autorisierten Wert.',
'confirmed' => 'Die Bestätigung des Feldes :attribute stimmt nicht überein.',
'contains' => 'Das Feld :attribute enthält keinen erforderlichen Wert.',
'current_password' => 'Das Passwort ist falsch.',
'date' => 'Das Feld :attribute muss ein gültiges Datum sein.',
'date_equals' => 'Das Feld :attribute muss ein Datum gleich :date sein.',
'date_format' => 'Das Feld :attribute muss dem Format :format entsprechen.',
'decimal' => 'Das Feld :attribute muss :decimal Dezimalstellen haben.',
'declined' => 'Das Feld :attribute muss abgelehnt werden.',
'declined_if' => 'Das Feld :attribute muss abgelehnt werden, wenn :other :value ist.',
'different' => 'Die Felder :attribute und :other müssen unterschiedlich sein.',
'digits' => 'Das Feld :attribute muss :digits Ziffern haben.',
'digits_between' => 'Das Feld :attribute muss zwischen :min und :max Ziffern haben.',
'dimensions' => 'Das Feld :attribute hat ungültige Bildabmessungen.',
'distinct' => 'Das Feld :attribute hat einen doppelten Wert.',
'doesnt_contain' => 'Das Feld :attribute darf Folgendes nicht enthalten: :values.',
'doesnt_end_with' => 'Das Feld :attribute darf nicht mit Folgendem enden: :values.',
'doesnt_start_with' => 'Das Feld :attribute darf nicht mit Folgendem beginnen: :values.',
'email' => 'Das Feld :attribute muss eine gültige E-Mail-Adresse sein.',
'encoding' => 'Das Feld :attribute muss in :encoding kodiert sein.',
'ends_with' => 'Das Feld :attribute muss mit Folgendem enden: :values.',
'enum' => 'Die ausgewählte :attribute ist ungültig.',
'exists' => 'Die ausgewählte :attribute ist ungültig.',
'extensions' => 'Das Feld :attribute muss eine der folgenden Erweiterungen haben: :values.',
'file' => 'Das Feld :attribute muss eine Datei sein.',
'filled' => 'Das Feld :attribute muss einen Wert haben.',
'gt' => [
'array' => 'Das Feld :attribute muss mehr als :value Elemente haben.',
'file' => 'Das Feld :attribute muss größer als :value Kilobyte sein.',
'numeric' => 'Das Feld :attribute muss größer als :value sein.',
'string' => 'Das Feld :attribute muss mehr als :value Zeichen haben.',
],
'gte' => [
'array' => 'Das Feld :attribute muss :value oder mehr Elemente haben.',
'file' => 'Das Feld :attribute muss größer oder gleich :value Kilobyte sein.',
'numeric' => 'Das Feld :attribute muss größer oder gleich :value sein.',
'string' => 'Das Feld :attribute muss :value oder mehr Zeichen haben.',
],
'hex_color' => 'Das Feld :attribute muss eine gültige Hexadezimalfarbe sein.',
'image' => 'Das Feld :attribute muss ein Bild sein.',
'in' => 'Die ausgewählte :attribute ist ungültig.',
'in_array' => 'Das Feld :attribute muss in :other existieren.',
'in_array_keys' => 'Das Feld :attribute muss mindestens einen der folgenden Schlüssel enthalten: :values.',
'integer' => 'Das Feld :attribute muss eine ganze Zahl sein.',
'ip' => 'Das Feld :attribute muss eine gültige IP-Adresse sein.',
'ipv4' => 'Das Feld :attribute muss eine gültige IPv4-Adresse sein.',
'ipv6' => 'Das Feld :attribute muss eine gültige IPv6-Adresse sein.',
'json' => 'Das Feld :attribute muss eine gültige JSON-Zeichenkette sein.',
'list' => 'Das Feld :attribute muss eine Liste sein.',
'lowercase' => 'Das Feld :attribute muss in Kleinbuchstaben sein.',
'lt' => [
'array' => 'Das Feld :attribute muss weniger als :value Elemente haben.',
'file' => 'Das Feld :attribute muss kleiner als :value Kilobyte sein.',
'numeric' => 'Das Feld :attribute muss kleiner als :value sein.',
'string' => 'Das Feld :attribute muss weniger als :value Zeichen haben.',
],
'lte' => [
'array' => 'Das Feld :attribute darf nicht mehr als :value Elemente haben.',
'file' => 'Das Feld :attribute muss kleiner oder gleich :value Kilobyte sein.',
'numeric' => 'Das Feld :attribute muss kleiner oder gleich :value sein.',
'string' => 'Das Feld :attribute darf nicht mehr als :value Zeichen haben.',
],
'mac_address' => 'Das Feld :attribute muss eine gültige MAC-Adresse sein.',
'max' => [
'array' => 'Das Feld :attribute darf nicht mehr als :max Elemente haben.',
'file' => 'Das Feld :attribute darf nicht größer als :max Kilobyte sein.',
'numeric' => 'Das Feld :attribute darf nicht größer als :max sein.',
'string' => 'Das Feld :attribute darf nicht mehr als :max Zeichen haben.',
],
'max_digits' => 'Das Feld :attribute darf nicht mehr als :max Ziffern haben.',
'mimes' => 'Das Feld :attribute muss eine Datei vom Typ :values sein.',
'mimetypes' => 'Das Feld :attribute muss eine Datei vom Typ :values sein.',
'min' => [
'array' => 'Das Feld :attribute muss mindestens :min Elemente haben.',
'file' => 'Das Feld :attribute muss mindestens :min Kilobyte sein.',
'numeric' => 'Das Feld :attribute muss mindestens :min sein.',
'string' => 'Das Feld :attribute muss mindestens :min Zeichen haben.',
],
'min_digits' => 'Das Feld :attribute muss mindestens :min Ziffern haben.',
'missing' => 'Das Feld :attribute muss fehlen.',
'missing_if' => 'Das Feld :attribute muss fehlen, wenn :other :value ist.',
'missing_unless' => 'Das Feld :attribute muss fehlen, es sei denn :other ist :value.',
'missing_with' => 'Das Feld :attribute muss fehlen, wenn :values vorhanden ist.',
'missing_with_all' => 'Das Feld :attribute muss fehlen, wenn :values vorhanden sind.',
'multiple_of' => 'Das Feld :attribute muss ein Vielfaches von :value sein.',
'not_in' => 'Die ausgewählte :attribute ist ungültig.',
'not_regex' => 'Das Format des Feldes :attribute ist ungültig.',
'numeric' => 'Das Feld :attribute muss eine Zahl sein.',
'password' => [
'letters' => 'Das Feld :attribute muss mindestens einen Buchstaben enthalten.',
'mixed' => 'Das Feld :attribute muss mindestens einen Groß- und einen Kleinbuchstaben enthalten.',
'numbers' => 'Das Feld :attribute muss mindestens eine Zahl enthalten.',
'symbols' => 'Das Feld :attribute muss mindestens ein Symbol enthalten.',
'uncompromised' => 'Die angegebene :attribute ist in einem Datenleck aufgetaucht. Bitte wählen Sie eine andere :attribute.',
],
'present' => 'Das Feld :attribute muss vorhanden sein.',
'present_if' => 'Das Feld :attribute muss vorhanden sein, wenn :other :value ist.',
'present_unless' => 'Das Feld :attribute muss vorhanden sein, es sei denn :other ist :value.',
'present_with' => 'Das Feld :attribute muss vorhanden sein, wenn :values vorhanden ist.',
'present_with_all' => 'Das Feld :attribute muss vorhanden sein, wenn :values vorhanden sind.',
'prohibited' => 'Das Feld :attribute ist verboten.',
'prohibited_if' => 'Das Feld :attribute ist verboten, wenn :other :value ist.',
'prohibited_if_accepted' => 'Das Feld :attribute ist verboten, wenn :other akzeptiert wird.',
'prohibited_if_declined' => 'Das Feld :attribute ist verboten, wenn :other abgelehnt wird.',
'prohibited_unless' => 'Das Feld :attribute ist verboten, es sei denn :other ist in :values.',
'prohibits' => 'Das Feld :attribute verbietet die Anwesenheit von :other.',
'regex' => 'Das Format des Feldes :attribute ist ungültig.',
'required' => 'Das Feld :attribute ist erforderlich.',
'required_array_keys' => 'Das Feld :attribute muss Einträge für Folgendes enthalten: :values.',
'required_if' => 'Das Feld :attribute ist erforderlich, wenn :other :value ist.',
'required_if_accepted' => 'Das Feld :attribute ist erforderlich, wenn :other akzeptiert wird.',
'required_if_declined' => 'Das Feld :attribute ist erforderlich, wenn :other abgelehnt wird.',
'required_unless' => 'Das Feld :attribute ist erforderlich, es sei denn :other ist in :values.',
'required_with' => 'Das Feld :attribute ist erforderlich, wenn :values vorhanden ist.',
'required_with_all' => 'Das Feld :attribute ist erforderlich, wenn :values vorhanden sind.',
'required_without' => 'Das Feld :attribute ist erforderlich, wenn :values nicht vorhanden ist.',
'required_without_all' => 'Das Feld :attribute ist erforderlich, wenn keines von :values vorhanden ist.',
'same' => 'Die Felder :attribute und :other müssen übereinstimmen.',
'size' => [
'array' => 'Das Feld :attribute muss :size Elemente enthalten.',
'file' => 'Das Feld :attribute muss :size Kilobyte sein.',
'numeric' => 'Das Feld :attribute muss :size sein.',
'string' => 'Das Feld :attribute muss :size Zeichen haben.',
],
'starts_with' => 'Das Feld :attribute muss mit Folgendem beginnen: :values.',
'string' => 'Das Feld :attribute muss eine Zeichenkette sein.',
'timezone' => 'Das Feld :attribute muss eine gültige Zeitzone sein.',
'unique' => 'Die :attribute wurde bereits verwendet.',
'uploaded' => 'Der Upload von :attribute ist fehlgeschlagen.',
'uppercase' => 'Das Feld :attribute muss in Großbuchstaben sein.',
'url' => 'Das Feld :attribute muss eine gültige URL sein.',
'ulid' => 'Das Feld :attribute muss eine gültige ULID sein.',
'uuid' => 'Das Feld :attribute muss eine gültige UUID sein.',
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
'attributes' => [],
];
@@ -0,0 +1,180 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
*/
'accepted' => 'The :attribute field must be accepted.',
'accepted_if' => 'The :attribute field must be accepted when :other is :value.',
'active_url' => 'The :attribute field must be a valid URL.',
'after' => 'The :attribute field must be a date after :date.',
'after_or_equal' => 'The :attribute field must be a date after or equal to :date.',
'alpha' => 'The :attribute field must only contain letters.',
'alpha_dash' => 'The :attribute field must only contain letters, numbers, dashes, and underscores.',
'alpha_num' => 'The :attribute field must only contain letters and numbers.',
'any_of' => 'The :attribute field is invalid.',
'array' => 'The :attribute field must be an array.',
'ascii' => 'The :attribute field must only contain single-byte alphanumeric characters and symbols.',
'before' => 'The :attribute field must be a date before :date.',
'before_or_equal' => 'The :attribute field must be a date before or equal to :date.',
'between' => [
'array' => 'The :attribute field must have between :min and :max items.',
'file' => 'The :attribute field must be between :min and :max kilobytes.',
'numeric' => 'The :attribute field must be between :min and :max.',
'string' => 'The :attribute field must be between :min and :max characters.',
],
'boolean' => 'The :attribute field must be true or false.',
'can' => 'The :attribute field contains an unauthorized value.',
'confirmed' => 'The :attribute field confirmation does not match.',
'contains' => 'The :attribute field is missing a required value.',
'current_password' => 'The password is incorrect.',
'date' => 'The :attribute field must be a valid date.',
'date_equals' => 'The :attribute field must be a date equal to :date.',
'date_format' => 'The :attribute field must match the format :format.',
'decimal' => 'The :attribute field must have :decimal decimal places.',
'declined' => 'The :attribute field must be declined.',
'declined_if' => 'The :attribute field must be declined when :other is :value.',
'different' => 'The :attribute field and :other must be different.',
'digits' => 'The :attribute field must be :digits digits.',
'digits_between' => 'The :attribute field must be between :min and :max digits.',
'dimensions' => 'The :attribute field has invalid image dimensions.',
'distinct' => 'The :attribute field has a duplicate value.',
'doesnt_contain' => 'The :attribute field must not contain any of the following: :values.',
'doesnt_end_with' => 'The :attribute field must not end with one of the following: :values.',
'doesnt_start_with' => 'The :attribute field must not start with one of the following: :values.',
'email' => 'The :attribute field must be a valid email address.',
'encoding' => 'The :attribute field must be encoded in :encoding.',
'ends_with' => 'The :attribute field must end with one of the following: :values.',
'enum' => 'The selected :attribute is invalid.',
'exists' => 'The selected :attribute is invalid.',
'extensions' => 'The :attribute field must have one of the following extensions: :values.',
'file' => 'The :attribute field must be a file.',
'filled' => 'The :attribute field must have a value.',
'gt' => [
'array' => 'The :attribute field must have more than :value items.',
'file' => 'The :attribute field must be greater than :value kilobytes.',
'numeric' => 'The :attribute field must be greater than :value.',
'string' => 'The :attribute field must be greater than :value characters.',
],
'gte' => [
'array' => 'The :attribute field must have :value items or more.',
'file' => 'The :attribute field must be greater than or equal to :value kilobytes.',
'numeric' => 'The :attribute field must be greater than or equal to :value.',
'string' => 'The :attribute field must be greater than or equal to :value characters.',
],
'hex_color' => 'The :attribute field must be a valid hexadecimal color.',
'image' => 'The :attribute field must be an image.',
'in' => 'The selected :attribute is invalid.',
'in_array' => 'The :attribute field must exist in :other.',
'in_array_keys' => 'The :attribute field must contain at least one of the following keys: :values.',
'integer' => 'The :attribute field must be an integer.',
'ip' => 'The :attribute field must be a valid IP address.',
'ipv4' => 'The :attribute field must be a valid IPv4 address.',
'ipv6' => 'The :attribute field must be a valid IPv6 address.',
'json' => 'The :attribute field must be a valid JSON string.',
'list' => 'The :attribute field must be a list.',
'lowercase' => 'The :attribute field must be lowercase.',
'lt' => [
'array' => 'The :attribute field must have less than :value items.',
'file' => 'The :attribute field must be less than :value kilobytes.',
'numeric' => 'The :attribute field must be less than :value.',
'string' => 'The :attribute field must be less than :value characters.',
],
'lte' => [
'array' => 'The :attribute field must not have more than :value items.',
'file' => 'The :attribute field must be less than or equal to :value kilobytes.',
'numeric' => 'The :attribute field must be less than or equal to :value.',
'string' => 'The :attribute field must be less than or equal to :value characters.',
],
'mac_address' => 'The :attribute field must be a valid MAC address.',
'max' => [
'array' => 'The :attribute field must not have more than :max items.',
'file' => 'The :attribute field must not be greater than :max kilobytes.',
'numeric' => 'The :attribute field must not be greater than :max.',
'string' => 'The :attribute field must not be greater than :max characters.',
],
'max_digits' => 'The :attribute field must not have more than :max digits.',
'mimes' => 'The :attribute field must be a file of type: :values.',
'mimetypes' => 'The :attribute field must be a file of type: :values.',
'min' => [
'array' => 'The :attribute field must have at least :min items.',
'file' => 'The :attribute field must be at least :min kilobytes.',
'numeric' => 'The :attribute field must be at least :min.',
'string' => 'The :attribute field must be at least :min characters.',
],
'min_digits' => 'The :attribute field must have at least :min digits.',
'missing' => 'The :attribute field must be missing.',
'missing_if' => 'The :attribute field must be missing when :other is :value.',
'missing_unless' => 'The :attribute field must be missing unless :other is :value.',
'missing_with' => 'The :attribute field must be missing when :values is present.',
'missing_with_all' => 'The :attribute field must be missing when :values are present.',
'multiple_of' => 'The :attribute field must be a multiple of :value.',
'not_in' => 'The selected :attribute is invalid.',
'not_regex' => 'The :attribute field format is invalid.',
'numeric' => 'The :attribute field must be a number.',
'password' => [
'letters' => 'The :attribute field must contain at least one letter.',
'mixed' => 'The :attribute field must contain at least one uppercase and one lowercase letter.',
'numbers' => 'The :attribute field must contain at least one number.',
'symbols' => 'The :attribute field must contain at least one symbol.',
'uncompromised' => 'The given :attribute has appeared in a data leak. Please choose a different :attribute.',
],
'present' => 'The :attribute field must be present.',
'present_if' => 'The :attribute field must be present when :other is :value.',
'present_unless' => 'The :attribute field must be present unless :other is :value.',
'present_with' => 'The :attribute field must be present when :values is present.',
'present_with_all' => 'The :attribute field must be present when :values are present.',
'prohibited' => 'The :attribute field is prohibited.',
'prohibited_if' => 'The :attribute field is prohibited when :other is :value.',
'prohibited_if_accepted' => 'The :attribute field is prohibited when :other is accepted.',
'prohibited_if_declined' => 'The :attribute field is prohibited when :other is declined.',
'prohibited_unless' => 'The :attribute field is prohibited unless :other is in :values.',
'prohibits' => 'The :attribute field prohibits :other from being present.',
'regex' => 'The :attribute field format is invalid.',
'required' => 'The :attribute field is required.',
'required_array_keys' => 'The :attribute field must contain entries for: :values.',
'required_if' => 'The :attribute field is required when :other is :value.',
'required_if_accepted' => 'The :attribute field is required when :other is accepted.',
'required_if_declined' => 'The :attribute field is required when :other is declined.',
'required_unless' => 'The :attribute field is required unless :other is in :values.',
'required_with' => 'The :attribute field is required when :values is present.',
'required_with_all' => 'The :attribute field is required when :values are present.',
'required_without' => 'The :attribute field is required when :values is not present.',
'required_without_all' => 'The :attribute field is required when none of :values are present.',
'same' => 'The :attribute field must match :other.',
'size' => [
'array' => 'The :attribute field must contain :size items.',
'file' => 'The :attribute field must be :size kilobytes.',
'numeric' => 'The :attribute field must be :size.',
'string' => 'The :attribute field must be :size characters.',
],
'starts_with' => 'The :attribute field must start with one of the following: :values.',
'string' => 'The :attribute field must be a string.',
'timezone' => 'The :attribute field must be a valid timezone.',
'unique' => 'The :attribute has already been taken.',
'uploaded' => 'The :attribute failed to upload.',
'uppercase' => 'The :attribute field must be uppercase.',
'url' => 'The :attribute field must be a valid URL.',
'ulid' => 'The :attribute field must be a valid ULID.',
'uuid' => 'The :attribute field must be a valid UUID.',
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
|--------------------------------------------------------------------------
*/
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
/*
|--------------------------------------------------------------------------
| Custom Validation Attributes
|--------------------------------------------------------------------------
*/
'attributes' => [],
];
@@ -0,0 +1,170 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Mensajes de validación
|--------------------------------------------------------------------------
*/
'accepted' => 'El campo :attribute debe ser aceptado.',
'accepted_if' => 'El campo :attribute debe ser aceptado cuando :other sea :value.',
'active_url' => 'El campo :attribute debe ser una URL válida.',
'after' => 'El campo :attribute debe ser una fecha posterior a :date.',
'after_or_equal' => 'El campo :attribute debe ser una fecha posterior o igual a :date.',
'alpha' => 'El campo :attribute solo debe contener letras.',
'alpha_dash' => 'El campo :attribute solo debe contener letras, números, guiones y guiones bajos.',
'alpha_num' => 'El campo :attribute solo debe contener letras y números.',
'any_of' => 'El campo :attribute no es válido.',
'array' => 'El campo :attribute debe ser un array.',
'ascii' => 'El campo :attribute solo debe contener caracteres alfanuméricos y símbolos de un byte.',
'before' => 'El campo :attribute debe ser una fecha anterior a :date.',
'before_or_equal' => 'El campo :attribute debe ser una fecha anterior o igual a :date.',
'between' => [
'array' => 'El campo :attribute debe tener entre :min y :max elementos.',
'file' => 'El campo :attribute debe tener entre :min y :max kilobytes.',
'numeric' => 'El campo :attribute debe estar entre :min y :max.',
'string' => 'El campo :attribute debe tener entre :min y :max caracteres.',
],
'boolean' => 'El campo :attribute debe ser verdadero o falso.',
'can' => 'El campo :attribute contiene un valor no autorizado.',
'confirmed' => 'La confirmación del campo :attribute no coincide.',
'contains' => 'El campo :attribute no contiene un valor requerido.',
'current_password' => 'La contraseña es incorrecta.',
'date' => 'El campo :attribute debe ser una fecha válida.',
'date_equals' => 'El campo :attribute debe ser una fecha igual a :date.',
'date_format' => 'El campo :attribute debe coincidir con el formato :format.',
'decimal' => 'El campo :attribute debe tener :decimal decimales.',
'declined' => 'El campo :attribute debe ser rechazado.',
'declined_if' => 'El campo :attribute debe ser rechazado cuando :other sea :value.',
'different' => 'Los campos :attribute y :other deben ser diferentes.',
'digits' => 'El campo :attribute debe tener :digits dígitos.',
'digits_between' => 'El campo :attribute debe tener entre :min y :max dígitos.',
'dimensions' => 'El campo :attribute tiene dimensiones de imagen inválidas.',
'distinct' => 'El campo :attribute tiene un valor duplicado.',
'doesnt_contain' => 'El campo :attribute no debe contener: :values.',
'doesnt_end_with' => 'El campo :attribute no debe terminar con: :values.',
'doesnt_start_with' => 'El campo :attribute no debe comenzar con: :values.',
'email' => 'El campo :attribute debe ser una dirección de correo electrónico válida.',
'encoding' => 'El campo :attribute debe estar codificado en :encoding.',
'ends_with' => 'El campo :attribute debe terminar con: :values.',
'enum' => 'El :attribute seleccionado no es válido.',
'exists' => 'El :attribute seleccionado no es válido.',
'extensions' => 'El campo :attribute debe tener una de las siguientes extensiones: :values.',
'file' => 'El campo :attribute debe ser un archivo.',
'filled' => 'El campo :attribute debe tener un valor.',
'gt' => [
'array' => 'El campo :attribute debe tener más de :value elementos.',
'file' => 'El campo :attribute debe ser mayor que :value kilobytes.',
'numeric' => 'El campo :attribute debe ser mayor que :value.',
'string' => 'El campo :attribute debe tener más de :value caracteres.',
],
'gte' => [
'array' => 'El campo :attribute debe tener :value elementos o más.',
'file' => 'El campo :attribute debe ser mayor o igual que :value kilobytes.',
'numeric' => 'El campo :attribute debe ser mayor o igual que :value.',
'string' => 'El campo :attribute debe tener :value caracteres o más.',
],
'hex_color' => 'El campo :attribute debe ser un color hexadecimal válido.',
'image' => 'El campo :attribute debe ser una imagen.',
'in' => 'El :attribute seleccionado no es válido.',
'in_array' => 'El campo :attribute debe existir en :other.',
'in_array_keys' => 'El campo :attribute debe contener al menos una de las siguientes claves: :values.',
'integer' => 'El campo :attribute debe ser un número entero.',
'ip' => 'El campo :attribute debe ser una dirección IP válida.',
'ipv4' => 'El campo :attribute debe ser una dirección IPv4 válida.',
'ipv6' => 'El campo :attribute debe ser una dirección IPv6 válida.',
'json' => 'El campo :attribute debe ser una cadena JSON válida.',
'list' => 'El campo :attribute debe ser una lista.',
'lowercase' => 'El campo :attribute debe estar en minúsculas.',
'lt' => [
'array' => 'El campo :attribute debe tener menos de :value elementos.',
'file' => 'El campo :attribute debe ser menor que :value kilobytes.',
'numeric' => 'El campo :attribute debe ser menor que :value.',
'string' => 'El campo :attribute debe tener menos de :value caracteres.',
],
'lte' => [
'array' => 'El campo :attribute no debe tener más de :value elementos.',
'file' => 'El campo :attribute debe ser menor o igual que :value kilobytes.',
'numeric' => 'El campo :attribute debe ser menor o igual que :value.',
'string' => 'El campo :attribute debe tener :value caracteres o menos.',
],
'mac_address' => 'El campo :attribute debe ser una dirección MAC válida.',
'max' => [
'array' => 'El campo :attribute no debe tener más de :max elementos.',
'file' => 'El campo :attribute no debe ser mayor que :max kilobytes.',
'numeric' => 'El campo :attribute no debe ser mayor que :max.',
'string' => 'El campo :attribute no debe tener más de :max caracteres.',
],
'max_digits' => 'El campo :attribute no debe tener más de :max dígitos.',
'mimes' => 'El campo :attribute debe ser un archivo de tipo: :values.',
'mimetypes' => 'El campo :attribute debe ser un archivo de tipo: :values.',
'min' => [
'array' => 'El campo :attribute debe tener al menos :min elementos.',
'file' => 'El campo :attribute debe tener al menos :min kilobytes.',
'numeric' => 'El campo :attribute debe ser al menos :min.',
'string' => 'El campo :attribute debe tener al menos :min caracteres.',
],
'min_digits' => 'El campo :attribute debe tener al menos :min dígitos.',
'missing' => 'El campo :attribute debe estar ausente.',
'missing_if' => 'El campo :attribute debe estar ausente cuando :other sea :value.',
'missing_unless' => 'El campo :attribute debe estar ausente a menos que :other sea :value.',
'missing_with' => 'El campo :attribute debe estar ausente cuando :values esté presente.',
'missing_with_all' => 'El campo :attribute debe estar ausente cuando :values estén presentes.',
'multiple_of' => 'El campo :attribute debe ser un múltiplo de :value.',
'not_in' => 'El :attribute seleccionado no es válido.',
'not_regex' => 'El formato del campo :attribute no es válido.',
'numeric' => 'El campo :attribute debe ser un número.',
'password' => [
'letters' => 'El campo :attribute debe contener al menos una letra.',
'mixed' => 'El campo :attribute debe contener al menos una mayúscula y una minúscula.',
'numbers' => 'El campo :attribute debe contener al menos un número.',
'symbols' => 'El campo :attribute debe contener al menos un símbolo.',
'uncompromised' => 'El :attribute proporcionado ha aparecido en una filtración de datos. Por favor elija un :attribute diferente.',
],
'present' => 'El campo :attribute debe estar presente.',
'present_if' => 'El campo :attribute debe estar presente cuando :other sea :value.',
'present_unless' => 'El campo :attribute debe estar presente a menos que :other sea :value.',
'present_with' => 'El campo :attribute debe estar presente cuando :values esté presente.',
'present_with_all' => 'El campo :attribute debe estar presente cuando :values estén presentes.',
'prohibited' => 'El campo :attribute está prohibido.',
'prohibited_if' => 'El campo :attribute está prohibido cuando :other sea :value.',
'prohibited_if_accepted' => 'El campo :attribute está prohibido cuando :other sea aceptado.',
'prohibited_if_declined' => 'El campo :attribute está prohibido cuando :other sea rechazado.',
'prohibited_unless' => 'El campo :attribute está prohibido a menos que :other esté en :values.',
'prohibits' => 'El campo :attribute prohíbe que :other esté presente.',
'regex' => 'El formato del campo :attribute no es válido.',
'required' => 'El campo :attribute es obligatorio.',
'required_array_keys' => 'El campo :attribute debe contener entradas para: :values.',
'required_if' => 'El campo :attribute es obligatorio cuando :other sea :value.',
'required_if_accepted' => 'El campo :attribute es obligatorio cuando :other sea aceptado.',
'required_if_declined' => 'El campo :attribute es obligatorio cuando :other sea rechazado.',
'required_unless' => 'El campo :attribute es obligatorio a menos que :other esté en :values.',
'required_with' => 'El campo :attribute es obligatorio cuando :values esté presente.',
'required_with_all' => 'El campo :attribute es obligatorio cuando :values estén presentes.',
'required_without' => 'El campo :attribute es obligatorio cuando :values no esté presente.',
'required_without_all' => 'El campo :attribute es obligatorio cuando ninguno de :values esté presente.',
'same' => 'Los campos :attribute y :other deben coincidir.',
'size' => [
'array' => 'El campo :attribute debe contener :size elementos.',
'file' => 'El campo :attribute debe tener :size kilobytes.',
'numeric' => 'El campo :attribute debe ser :size.',
'string' => 'El campo :attribute debe tener :size caracteres.',
],
'starts_with' => 'El campo :attribute debe comenzar con: :values.',
'string' => 'El campo :attribute debe ser una cadena de caracteres.',
'timezone' => 'El campo :attribute debe ser una zona horaria válida.',
'unique' => 'El :attribute ya ha sido utilizado.',
'uploaded' => 'La subida del :attribute ha fallado.',
'uppercase' => 'El campo :attribute debe estar en mayúsculas.',
'url' => 'El campo :attribute debe ser una URL válida.',
'ulid' => 'El campo :attribute debe ser un ULID válido.',
'uuid' => 'El campo :attribute debe ser un UUID válido.',
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
'attributes' => [],
];
@@ -0,0 +1,170 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Messages de validation
|--------------------------------------------------------------------------
*/
'accepted' => 'Le champ :attribute doit être accepté.',
'accepted_if' => 'Le champ :attribute doit être accepté lorsque :other est :value.',
'active_url' => 'Le champ :attribute doit être une URL valide.',
'after' => 'Le champ :attribute doit être une date postérieure à :date.',
'after_or_equal' => 'Le champ :attribute doit être une date postérieure ou égale à :date.',
'alpha' => 'Le champ :attribute ne doit contenir que des lettres.',
'alpha_dash' => 'Le champ :attribute ne doit contenir que des lettres, des chiffres, des tirets et des underscores.',
'alpha_num' => 'Le champ :attribute ne doit contenir que des lettres et des chiffres.',
'any_of' => 'Le champ :attribute est invalide.',
'array' => 'Le champ :attribute doit être un tableau.',
'ascii' => 'Le champ :attribute ne doit contenir que des caractères alphanumériques et des symboles sur un octet.',
'before' => 'Le champ :attribute doit être une date antérieure à :date.',
'before_or_equal' => 'Le champ :attribute doit être une date antérieure ou égale à :date.',
'between' => [
'array' => 'Le champ :attribute doit contenir entre :min et :max éléments.',
'file' => 'Le champ :attribute doit être entre :min et :max kilo-octets.',
'numeric' => 'Le champ :attribute doit être entre :min et :max.',
'string' => 'Le champ :attribute doit contenir entre :min et :max caractères.',
],
'boolean' => 'Le champ :attribute doit être vrai ou faux.',
'can' => 'Le champ :attribute contient une valeur non autorisée.',
'confirmed' => 'La confirmation du champ :attribute ne correspond pas.',
'contains' => 'Le champ :attribute ne contient pas une valeur requise.',
'current_password' => 'Le mot de passe est incorrect.',
'date' => 'Le champ :attribute doit être une date valide.',
'date_equals' => 'Le champ :attribute doit être une date égale à :date.',
'date_format' => 'Le champ :attribute doit correspondre au format :format.',
'decimal' => 'Le champ :attribute doit avoir :decimal décimales.',
'declined' => 'Le champ :attribute doit être refusé.',
'declined_if' => 'Le champ :attribute doit être refusé lorsque :other est :value.',
'different' => 'Les champs :attribute et :other doivent être différents.',
'digits' => 'Le champ :attribute doit contenir :digits chiffres.',
'digits_between' => 'Le champ :attribute doit contenir entre :min et :max chiffres.',
'dimensions' => 'Le champ :attribute a des dimensions d\'image invalides.',
'distinct' => 'Le champ :attribute a une valeur en double.',
'doesnt_contain' => 'Le champ :attribute ne doit pas contenir :values.',
'doesnt_end_with' => 'Le champ :attribute ne doit pas se terminer par :values.',
'doesnt_start_with' => 'Le champ :attribute ne doit pas commencer par :values.',
'email' => 'Le champ :attribute doit être une adresse e-mail valide.',
'encoding' => 'Le champ :attribute doit être encodé en :encoding.',
'ends_with' => 'Le champ :attribute doit se terminer par :values.',
'enum' => 'Le :attribute sélectionné est invalide.',
'exists' => 'Le :attribute sélectionné est invalide.',
'extensions' => 'Le champ :attribute doit avoir l\'une des extensions suivantes : :values.',
'file' => 'Le champ :attribute doit être un fichier.',
'filled' => 'Le champ :attribute doit avoir une valeur.',
'gt' => [
'array' => 'Le champ :attribute doit contenir plus de :value éléments.',
'file' => 'Le champ :attribute doit être supérieur à :value kilo-octets.',
'numeric' => 'Le champ :attribute doit être supérieur à :value.',
'string' => 'Le champ :attribute doit contenir plus de :value caractères.',
],
'gte' => [
'array' => 'Le champ :attribute doit contenir :value éléments ou plus.',
'file' => 'Le champ :attribute doit être supérieur ou égal à :value kilo-octets.',
'numeric' => 'Le champ :attribute doit être supérieur ou égal à :value.',
'string' => 'Le champ :attribute doit contenir :value caractères ou plus.',
],
'hex_color' => 'Le champ :attribute doit être une couleur hexadécimale valide.',
'image' => 'Le champ :attribute doit être une image.',
'in' => 'Le :attribute sélectionné est invalide.',
'in_array' => 'Le champ :attribute doit exister dans :other.',
'in_array_keys' => 'Le champ :attribute doit contenir au moins une des clés suivantes : :values.',
'integer' => 'Le champ :attribute doit être un entier.',
'ip' => 'Le champ :attribute doit être une adresse IP valide.',
'ipv4' => 'Le champ :attribute doit être une adresse IPv4 valide.',
'ipv6' => 'Le champ :attribute doit être une adresse IPv6 valide.',
'json' => 'Le champ :attribute doit être une chaîne JSON valide.',
'list' => 'Le champ :attribute doit être une liste.',
'lowercase' => 'Le champ :attribute doit être en minuscules.',
'lt' => [
'array' => 'Le champ :attribute doit contenir moins de :value éléments.',
'file' => 'Le champ :attribute doit être inférieur à :value kilo-octets.',
'numeric' => 'Le champ :attribute doit être inférieur à :value.',
'string' => 'Le champ :attribute doit contenir moins de :value caractères.',
],
'lte' => [
'array' => 'Le champ :attribute ne doit pas contenir plus de :value éléments.',
'file' => 'Le champ :attribute doit être inférieur ou égal à :value kilo-octets.',
'numeric' => 'Le champ :attribute doit être inférieur ou égal à :value.',
'string' => 'Le champ :attribute doit contenir :value caractères ou moins.',
],
'mac_address' => 'Le champ :attribute doit être une adresse MAC valide.',
'max' => [
'array' => 'Le champ :attribute ne doit pas contenir plus de :max éléments.',
'file' => 'Le champ :attribute ne doit pas dépasser :max kilo-octets.',
'numeric' => 'Le champ :attribute ne doit pas être supérieur à :max.',
'string' => 'Le champ :attribute ne doit pas contenir plus de :max caractères.',
],
'max_digits' => 'Le champ :attribute ne doit pas contenir plus de :max chiffres.',
'mimes' => 'Le champ :attribute doit être un fichier de type : :values.',
'mimetypes' => 'Le champ :attribute doit être un fichier de type : :values.',
'min' => [
'array' => 'Le champ :attribute doit contenir au moins :min éléments.',
'file' => 'Le champ :attribute doit faire au moins :min kilo-octets.',
'numeric' => 'Le champ :attribute doit être au moins :min.',
'string' => 'Le champ :attribute doit contenir au moins :min caractères.',
],
'min_digits' => 'Le champ :attribute doit contenir au moins :min chiffres.',
'missing' => 'Le champ :attribute doit être absent.',
'missing_if' => 'Le champ :attribute doit être absent lorsque :other est :value.',
'missing_unless' => 'Le champ :attribute doit être absent sauf si :other est :value.',
'missing_with' => 'Le champ :attribute doit être absent lorsque :values est présent.',
'missing_with_all' => 'Le champ :attribute doit être absent lorsque :values sont présents.',
'multiple_of' => 'Le champ :attribute doit être un multiple de :value.',
'not_in' => 'Le :attribute sélectionné est invalide.',
'not_regex' => 'Le format du champ :attribute est invalide.',
'numeric' => 'Le champ :attribute doit être un nombre.',
'password' => [
'letters' => 'Le champ :attribute doit contenir au moins une lettre.',
'mixed' => 'Le champ :attribute doit contenir au moins une majuscule et une minuscule.',
'numbers' => 'Le champ :attribute doit contenir au moins un chiffre.',
'symbols' => 'Le champ :attribute doit contenir au moins un symbole.',
'uncompromised' => 'Le :attribute donné est apparu dans une fuite de données. Veuillez choisir un autre :attribute.',
],
'present' => 'Le champ :attribute doit être présent.',
'present_if' => 'Le champ :attribute doit être présent lorsque :other est :value.',
'present_unless' => 'Le champ :attribute doit être présent sauf si :other est :value.',
'present_with' => 'Le champ :attribute doit être présent lorsque :values est présent.',
'present_with_all' => 'Le champ :attribute doit être présent lorsque :values sont présents.',
'prohibited' => 'Le champ :attribute est interdit.',
'prohibited_if' => 'Le champ :attribute est interdit lorsque :other est :value.',
'prohibited_if_accepted' => 'Le champ :attribute est interdit lorsque :other est accepté.',
'prohibited_if_declined' => 'Le champ :attribute est interdit lorsque :other est refusé.',
'prohibited_unless' => 'Le champ :attribute est interdit sauf si :other est dans :values.',
'prohibits' => 'Le champ :attribute interdit la présence de :other.',
'regex' => 'Le format du champ :attribute est invalide.',
'required' => 'Le champ :attribute est obligatoire.',
'required_array_keys' => 'Le champ :attribute doit contenir les entrées suivantes : :values.',
'required_if' => 'Le champ :attribute est obligatoire lorsque :other est :value.',
'required_if_accepted' => 'Le champ :attribute est obligatoire lorsque :other est accepté.',
'required_if_declined' => 'Le champ :attribute est obligatoire lorsque :other est refusé.',
'required_unless' => 'Le champ :attribute est obligatoire sauf si :other est dans :values.',
'required_with' => 'Le champ :attribute est obligatoire lorsque :values est présent.',
'required_with_all' => 'Le champ :attribute est obligatoire lorsque :values sont présents.',
'required_without' => 'Le champ :attribute est obligatoire lorsque :values n\'est pas présent.',
'required_without_all' => 'Le champ :attribute est obligatoire lorsqu\'aucun de :values n\'est présent.',
'same' => 'Les champs :attribute et :other doivent correspondre.',
'size' => [
'array' => 'Le champ :attribute doit contenir :size éléments.',
'file' => 'Le champ :attribute doit faire :size kilo-octets.',
'numeric' => 'Le champ :attribute doit être :size.',
'string' => 'Le champ :attribute doit contenir :size caractères.',
],
'starts_with' => 'Le champ :attribute doit commencer par :values.',
'string' => 'Le champ :attribute doit être une chaîne de caractères.',
'timezone' => 'Le champ :attribute doit être un fuseau horaire valide.',
'unique' => 'Le :attribute a déjà été utilisé.',
'uploaded' => 'Le téléchargement du :attribute a échoué.',
'uppercase' => 'Le champ :attribute doit être en majuscules.',
'url' => 'Le champ :attribute doit être une URL valide.',
'ulid' => 'Le champ :attribute doit être un ULID valide.',
'uuid' => 'Le champ :attribute doit être un UUID valide.',
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
'attributes' => [],
];
@@ -0,0 +1,170 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Pesan validasi
|--------------------------------------------------------------------------
*/
'accepted' => 'Bidang :attribute harus diterima.',
'accepted_if' => 'Bidang :attribute harus diterima ketika :other adalah :value.',
'active_url' => 'Bidang :attribute harus berupa URL yang valid.',
'after' => 'Bidang :attribute harus berupa tanggal setelah :date.',
'after_or_equal' => 'Bidang :attribute harus berupa tanggal setelah atau sama dengan :date.',
'alpha' => 'Bidang :attribute hanya boleh berisi huruf.',
'alpha_dash' => 'Bidang :attribute hanya boleh berisi huruf, angka, tanda hubung, dan garis bawah.',
'alpha_num' => 'Bidang :attribute hanya boleh berisi huruf dan angka.',
'any_of' => 'Bidang :attribute tidak valid.',
'array' => 'Bidang :attribute harus berupa array.',
'ascii' => 'Bidang :attribute hanya boleh berisi karakter alfanumerik dan simbol satu byte.',
'before' => 'Bidang :attribute harus berupa tanggal sebelum :date.',
'before_or_equal' => 'Bidang :attribute harus berupa tanggal sebelum atau sama dengan :date.',
'between' => [
'array' => 'Bidang :attribute harus memiliki antara :min dan :max item.',
'file' => 'Bidang :attribute harus antara :min dan :max kilobyte.',
'numeric' => 'Bidang :attribute harus antara :min dan :max.',
'string' => 'Bidang :attribute harus antara :min dan :max karakter.',
],
'boolean' => 'Bidang :attribute harus benar atau salah.',
'can' => 'Bidang :attribute berisi nilai yang tidak diizinkan.',
'confirmed' => 'Konfirmasi bidang :attribute tidak cocok.',
'contains' => 'Bidang :attribute tidak berisi nilai yang wajib.',
'current_password' => 'Kata sandi salah.',
'date' => 'Bidang :attribute harus berupa tanggal yang valid.',
'date_equals' => 'Bidang :attribute harus berupa tanggal yang sama dengan :date.',
'date_format' => 'Bidang :attribute harus sesuai format :format.',
'decimal' => 'Bidang :attribute harus memiliki :decimal tempat desimal.',
'declined' => 'Bidang :attribute harus ditolak.',
'declined_if' => 'Bidang :attribute harus ditolak ketika :other adalah :value.',
'different' => 'Bidang :attribute dan :other harus berbeda.',
'digits' => 'Bidang :attribute harus :digits digit.',
'digits_between' => 'Bidang :attribute harus antara :min dan :max digit.',
'dimensions' => 'Bidang :attribute memiliki dimensi gambar yang tidak valid.',
'distinct' => 'Bidang :attribute memiliki nilai duplikat.',
'doesnt_contain' => 'Bidang :attribute tidak boleh berisi: :values.',
'doesnt_end_with' => 'Bidang :attribute tidak boleh diakhiri dengan: :values.',
'doesnt_start_with' => 'Bidang :attribute tidak boleh diawali dengan: :values.',
'email' => 'Bidang :attribute harus berupa alamat email yang valid.',
'encoding' => 'Bidang :attribute harus dikodekan dalam :encoding.',
'ends_with' => 'Bidang :attribute harus diakhiri dengan: :values.',
'enum' => ':attribute yang dipilih tidak valid.',
'exists' => ':attribute yang dipilih tidak valid.',
'extensions' => 'Bidang :attribute harus memiliki salah satu ekstensi berikut: :values.',
'file' => 'Bidang :attribute harus berupa file.',
'filled' => 'Bidang :attribute harus memiliki nilai.',
'gt' => [
'array' => 'Bidang :attribute harus memiliki lebih dari :value item.',
'file' => 'Bidang :attribute harus lebih besar dari :value kilobyte.',
'numeric' => 'Bidang :attribute harus lebih besar dari :value.',
'string' => 'Bidang :attribute harus lebih dari :value karakter.',
],
'gte' => [
'array' => 'Bidang :attribute harus memiliki :value item atau lebih.',
'file' => 'Bidang :attribute harus lebih besar atau sama dengan :value kilobyte.',
'numeric' => 'Bidang :attribute harus lebih besar atau sama dengan :value.',
'string' => 'Bidang :attribute harus :value karakter atau lebih.',
],
'hex_color' => 'Bidang :attribute harus berupa warna heksadesimal yang valid.',
'image' => 'Bidang :attribute harus berupa gambar.',
'in' => ':attribute yang dipilih tidak valid.',
'in_array' => 'Bidang :attribute harus ada di :other.',
'in_array_keys' => 'Bidang :attribute harus berisi setidaknya salah satu kunci berikut: :values.',
'integer' => 'Bidang :attribute harus berupa bilangan bulat.',
'ip' => 'Bidang :attribute harus berupa alamat IP yang valid.',
'ipv4' => 'Bidang :attribute harus berupa alamat IPv4 yang valid.',
'ipv6' => 'Bidang :attribute harus berupa alamat IPv6 yang valid.',
'json' => 'Bidang :attribute harus berupa string JSON yang valid.',
'list' => 'Bidang :attribute harus berupa daftar.',
'lowercase' => 'Bidang :attribute harus huruf kecil.',
'lt' => [
'array' => 'Bidang :attribute harus memiliki kurang dari :value item.',
'file' => 'Bidang :attribute harus kurang dari :value kilobyte.',
'numeric' => 'Bidang :attribute harus kurang dari :value.',
'string' => 'Bidang :attribute harus kurang dari :value karakter.',
],
'lte' => [
'array' => 'Bidang :attribute tidak boleh memiliki lebih dari :value item.',
'file' => 'Bidang :attribute harus kurang dari atau sama dengan :value kilobyte.',
'numeric' => 'Bidang :attribute harus kurang dari atau sama dengan :value.',
'string' => 'Bidang :attribute harus :value karakter atau kurang.',
],
'mac_address' => 'Bidang :attribute harus berupa alamat MAC yang valid.',
'max' => [
'array' => 'Bidang :attribute tidak boleh memiliki lebih dari :max item.',
'file' => 'Bidang :attribute tidak boleh lebih besar dari :max kilobyte.',
'numeric' => 'Bidang :attribute tidak boleh lebih besar dari :max.',
'string' => 'Bidang :attribute tidak boleh lebih dari :max karakter.',
],
'max_digits' => 'Bidang :attribute tidak boleh memiliki lebih dari :max digit.',
'mimes' => 'Bidang :attribute harus berupa file bertipe: :values.',
'mimetypes' => 'Bidang :attribute harus berupa file bertipe: :values.',
'min' => [
'array' => 'Bidang :attribute harus memiliki setidaknya :min item.',
'file' => 'Bidang :attribute harus setidaknya :min kilobyte.',
'numeric' => 'Bidang :attribute harus setidaknya :min.',
'string' => 'Bidang :attribute harus setidaknya :min karakter.',
],
'min_digits' => 'Bidang :attribute harus memiliki setidaknya :min digit.',
'missing' => 'Bidang :attribute harus tidak ada.',
'missing_if' => 'Bidang :attribute harus tidak ada ketika :other adalah :value.',
'missing_unless' => 'Bidang :attribute harus tidak ada kecuali :other adalah :value.',
'missing_with' => 'Bidang :attribute harus tidak ada ketika :values ada.',
'missing_with_all' => 'Bidang :attribute harus tidak ada ketika :values ada.',
'multiple_of' => 'Bidang :attribute harus kelipatan dari :value.',
'not_in' => ':attribute yang dipilih tidak valid.',
'not_regex' => 'Format bidang :attribute tidak valid.',
'numeric' => 'Bidang :attribute harus berupa angka.',
'password' => [
'letters' => 'Bidang :attribute harus berisi setidaknya satu huruf.',
'mixed' => 'Bidang :attribute harus berisi setidaknya satu huruf besar dan satu huruf kecil.',
'numbers' => 'Bidang :attribute harus berisi setidaknya satu angka.',
'symbols' => 'Bidang :attribute harus berisi setidaknya satu simbol.',
'uncompromised' => ':attribute yang diberikan telah muncul dalam kebocoran data. Silakan pilih :attribute yang berbeda.',
],
'present' => 'Bidang :attribute harus ada.',
'present_if' => 'Bidang :attribute harus ada ketika :other adalah :value.',
'present_unless' => 'Bidang :attribute harus ada kecuali :other adalah :value.',
'present_with' => 'Bidang :attribute harus ada ketika :values ada.',
'present_with_all' => 'Bidang :attribute harus ada ketika :values ada.',
'prohibited' => 'Bidang :attribute dilarang.',
'prohibited_if' => 'Bidang :attribute dilarang ketika :other adalah :value.',
'prohibited_if_accepted' => 'Bidang :attribute dilarang ketika :other diterima.',
'prohibited_if_declined' => 'Bidang :attribute dilarang ketika :other ditolak.',
'prohibited_unless' => 'Bidang :attribute dilarang kecuali :other ada di :values.',
'prohibits' => 'Bidang :attribute melarang :other untuk ada.',
'regex' => 'Format bidang :attribute tidak valid.',
'required' => 'Bidang :attribute wajib diisi.',
'required_array_keys' => 'Bidang :attribute harus berisi entri untuk: :values.',
'required_if' => 'Bidang :attribute wajib diisi ketika :other adalah :value.',
'required_if_accepted' => 'Bidang :attribute wajib diisi ketika :other diterima.',
'required_if_declined' => 'Bidang :attribute wajib diisi ketika :other ditolak.',
'required_unless' => 'Bidang :attribute wajib diisi kecuali :other ada di :values.',
'required_with' => 'Bidang :attribute wajib diisi ketika :values ada.',
'required_with_all' => 'Bidang :attribute wajib diisi ketika :values ada.',
'required_without' => 'Bidang :attribute wajib diisi ketika :values tidak ada.',
'required_without_all' => 'Bidang :attribute wajib diisi ketika tidak ada :values.',
'same' => 'Bidang :attribute dan :other harus cocok.',
'size' => [
'array' => 'Bidang :attribute harus berisi :size item.',
'file' => 'Bidang :attribute harus :size kilobyte.',
'numeric' => 'Bidang :attribute harus :size.',
'string' => 'Bidang :attribute harus :size karakter.',
],
'starts_with' => 'Bidang :attribute harus diawali dengan: :values.',
'string' => 'Bidang :attribute harus berupa string.',
'timezone' => 'Bidang :attribute harus berupa zona waktu yang valid.',
'unique' => ':attribute sudah digunakan.',
'uploaded' => 'Unggah :attribute gagal.',
'uppercase' => 'Bidang :attribute harus huruf besar.',
'url' => 'Bidang :attribute harus berupa URL yang valid.',
'ulid' => 'Bidang :attribute harus berupa ULID yang valid.',
'uuid' => 'Bidang :attribute harus berupa UUID yang valid.',
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
'attributes' => [],
];
@@ -0,0 +1,170 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| バリデーション文言
|--------------------------------------------------------------------------
*/
'accepted' => ':attribute を受け入れる必要があります。',
'accepted_if' => ':other が :value の場合、:attribute を受け入れる必要があります。',
'active_url' => ':attribute は有効なURLである必要があります。',
'after' => ':attribute は :date より後の日付である必要があります。',
'after_or_equal' => ':attribute は :date 以降の日付である必要があります。',
'alpha' => ':attribute は英字のみを含む必要があります。',
'alpha_dash' => ':attribute は英字、数字、ハイフン、アンダースコアのみを含む必要があります。',
'alpha_num' => ':attribute は英字と数字のみを含む必要があります。',
'any_of' => ':attribute は無効です。',
'array' => ':attribute は配列である必要があります。',
'ascii' => ':attribute はASCII文字のみを含む必要があります。',
'before' => ':attribute は :date より前の日付である必要があります。',
'before_or_equal' => ':attribute は :date 以前の日付である必要があります。',
'between' => [
'array' => ':attribute は :min から :max 個である必要があります。',
'file' => ':attribute は :min から :max KB である必要があります。',
'numeric' => ':attribute は :min から :max の間である必要があります。',
'string' => ':attribute は :min から :max 文字である必要があります。',
],
'boolean' => ':attribute は真偽値である必要があります。',
'can' => ':attribute に許可されていない値が含まれています。',
'confirmed' => ':attribute の確認が一致しません。',
'contains' => ':attribute に必須の値が含まれていません。',
'current_password' => 'パスワードが正しくありません。',
'date' => ':attribute は有効な日付である必要があります。',
'date_equals' => ':attribute は :date と等しい日付である必要があります。',
'date_format' => ':attribute は :format 形式である必要があります。',
'decimal' => ':attribute は :decimal 桁の小数である必要があります。',
'declined' => ':attribute は拒否する必要があります。',
'declined_if' => ':other が :value の場合、:attribute は拒否する必要があります。',
'different' => ':attribute と :other は異なる必要があります。',
'digits' => ':attribute は :digits 桁の数字である必要があります。',
'digits_between' => ':attribute は :min から :max 桁の数字である必要があります。',
'dimensions' => ':attribute の画像サイズが無効です。',
'distinct' => ':attribute に重複した値があります。',
'doesnt_contain' => ':attribute に以下を含めることはできません::values。',
'doesnt_end_with' => ':attribute は以下で終わってはいけません::values。',
'doesnt_start_with' => ':attribute は以下で始まってはいけません::values。',
'email' => ':attribute は有効なメールアドレスである必要があります。',
'encoding' => ':attribute は :encoding でエンコードされている必要があります。',
'ends_with' => ':attribute は以下で終わる必要があります::values。',
'enum' => '選択した :attribute は無効です。',
'exists' => '選択した :attribute は無効です。',
'extensions' => ':attribute は以下の拡張子のいずれかである必要があります::values。',
'file' => ':attribute はファイルである必要があります。',
'filled' => ':attribute には値が必要です。',
'gt' => [
'array' => ':attribute は :value 個より多い必要があります。',
'file' => ':attribute は :value KB より大きい必要があります。',
'numeric' => ':attribute は :value より大きい必要があります。',
'string' => ':attribute は :value 文字より多い必要があります。',
],
'gte' => [
'array' => ':attribute は :value 個以上である必要があります。',
'file' => ':attribute は :value KB 以上である必要があります。',
'numeric' => ':attribute は :value 以上である必要があります。',
'string' => ':attribute は :value 文字以上である必要があります。',
],
'hex_color' => ':attribute は有効な16進数カラーである必要があります。',
'image' => ':attribute は画像である必要があります。',
'in' => '選択した :attribute は無効です。',
'in_array' => ':attribute は :other に存在する必要があります。',
'in_array_keys' => ':attribute は以下のキーのいずれかを含む必要があります::values。',
'integer' => ':attribute は整数である必要があります。',
'ip' => ':attribute は有効なIPアドレスである必要があります。',
'ipv4' => ':attribute は有効なIPv4アドレスである必要があります。',
'ipv6' => ':attribute は有効なIPv6アドレスである必要があります。',
'json' => ':attribute は有効なJSON文字列である必要があります。',
'list' => ':attribute はリストである必要があります。',
'lowercase' => ':attribute は小文字である必要があります。',
'lt' => [
'array' => ':attribute は :value 個未満である必要があります。',
'file' => ':attribute は :value KB 未満である必要があります。',
'numeric' => ':attribute は :value 未満である必要があります。',
'string' => ':attribute は :value 文字未満である必要があります。',
],
'lte' => [
'array' => ':attribute は :value 個以下である必要があります。',
'file' => ':attribute は :value KB 以下である必要があります。',
'numeric' => ':attribute は :value 以下である必要があります。',
'string' => ':attribute は :value 文字以下である必要があります。',
],
'mac_address' => ':attribute は有効なMACアドレスである必要があります。',
'max' => [
'array' => ':attribute は :max 個以下である必要があります。',
'file' => ':attribute は :max KB 以下である必要があります。',
'numeric' => ':attribute は :max 以下である必要があります。',
'string' => ':attribute は :max 文字以下である必要があります。',
],
'max_digits' => ':attribute は :max 桁以下である必要があります。',
'mimes' => ':attribute は :values タイプのファイルである必要があります。',
'mimetypes' => ':attribute は :values タイプのファイルである必要があります。',
'min' => [
'array' => ':attribute は少なくとも :min 個である必要があります。',
'file' => ':attribute は少なくとも :min KB である必要があります。',
'numeric' => ':attribute は少なくとも :min である必要があります。',
'string' => ':attribute は少なくとも :min 文字である必要があります。',
],
'min_digits' => ':attribute は少なくとも :min 桁である必要があります。',
'missing' => ':attribute は存在してはいけません。',
'missing_if' => ':other が :value の場合、:attribute は存在してはいけません。',
'missing_unless' => ':other が :value でない限り、:attribute は存在してはいけません。',
'missing_with' => ':values が存在する場合、:attribute は存在してはいけません。',
'missing_with_all' => ':values がすべて存在する場合、:attribute は存在してはいけません。',
'multiple_of' => ':attribute は :value の倍数である必要があります。',
'not_in' => '選択した :attribute は無効です。',
'not_regex' => ':attribute の形式が無効です。',
'numeric' => ':attribute は数値である必要があります。',
'password' => [
'letters' => ':attribute は少なくとも1文字の英字を含む必要があります。',
'mixed' => ':attribute は少なくとも1つの大文字と1つの小文字を含む必要があります。',
'numbers' => ':attribute は少なくとも1つの数字を含む必要があります。',
'symbols' => ':attribute は少なくとも1つの記号を含む必要があります。',
'uncompromised' => ':attribute はデータ漏洩で発見されました。別の :attribute を選択してください。',
],
'present' => ':attribute は存在する必要があります。',
'present_if' => ':other が :value の場合、:attribute は存在する必要があります。',
'present_unless' => ':other が :value でない限り、:attribute は存在する必要があります。',
'present_with' => ':values が存在する場合、:attribute は存在する必要があります。',
'present_with_all' => ':values がすべて存在する場合、:attribute は存在する必要があります。',
'prohibited' => ':attribute は禁止されています。',
'prohibited_if' => ':other が :value の場合、:attribute は禁止されています。',
'prohibited_if_accepted' => ':other が受け入れられた場合、:attribute は禁止されています。',
'prohibited_if_declined' => ':other が拒否された場合、:attribute は禁止されています。',
'prohibited_unless' => ':other が :values に含まれていない限り、:attribute は禁止されています。',
'prohibits' => ':attribute が存在する場合、:other は禁止されています。',
'regex' => ':attribute の形式が無効です。',
'required' => ':attribute は必須です。',
'required_array_keys' => ':attribute は以下のキーを含む必要があります::values。',
'required_if' => ':other が :value の場合、:attribute は必須です。',
'required_if_accepted' => ':other が受け入れられた場合、:attribute は必須です。',
'required_if_declined' => ':other が拒否された場合、:attribute は必須です。',
'required_unless' => ':other が :values に含まれていない限り、:attribute は必須です。',
'required_with' => ':values が存在する場合、:attribute は必須です。',
'required_with_all' => ':values がすべて存在する場合、:attribute は必須です。',
'required_without' => ':values が存在しない場合、:attribute は必須です。',
'required_without_all' => ':values がすべて存在しない場合、:attribute は必須です。',
'same' => ':attribute と :other は一致する必要があります。',
'size' => [
'array' => ':attribute は :size 個である必要があります。',
'file' => ':attribute は :size KB である必要があります。',
'numeric' => ':attribute は :size である必要があります。',
'string' => ':attribute は :size 文字である必要があります。',
],
'starts_with' => ':attribute は以下で始まる必要があります::values。',
'string' => ':attribute は文字列である必要があります。',
'timezone' => ':attribute は有効なタイムゾーンである必要があります。',
'unique' => ':attribute は既に使用されています。',
'uploaded' => ':attribute のアップロードに失敗しました。',
'uppercase' => ':attribute は大文字である必要があります。',
'url' => ':attribute は有効なURLである必要があります。',
'ulid' => ':attribute は有効なULIDである必要があります。',
'uuid' => ':attribute は有効なUUIDである必要があります。',
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
'attributes' => [],
];
@@ -0,0 +1,170 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| 유효성 검사 메시지
|--------------------------------------------------------------------------
*/
'accepted' => ':attribute을(를) 수락해야 합니다.',
'accepted_if' => ':other이(가) :value일 때 :attribute을(를) 수락해야 합니다.',
'active_url' => ':attribute은(는) 유효한 URL이어야 합니다.',
'after' => ':attribute은(는) :date 이후 날짜여야 합니다.',
'after_or_equal' => ':attribute은(는) :date 이후이거나 같은 날짜여야 합니다.',
'alpha' => ':attribute은(는) 문자만 포함해야 합니다.',
'alpha_dash' => ':attribute은(는) 문자, 숫자, 대시, 밑줄만 포함해야 합니다.',
'alpha_num' => ':attribute은(는) 문자와 숫자만 포함해야 합니다.',
'any_of' => ':attribute이(가) 유효하지 않습니다.',
'array' => ':attribute은(는) 배열이어야 합니다.',
'ascii' => ':attribute은(는) ASCII 문자만 포함해야 합니다.',
'before' => ':attribute은(는) :date 이전 날짜여야 합니다.',
'before_or_equal' => ':attribute은(는) :date 이전이거나 같은 날짜여야 합니다.',
'between' => [
'array' => ':attribute은(는) :min개에서 :max개 사이여야 합니다.',
'file' => ':attribute은(는) :min KB에서 :max KB 사이여야 합니다.',
'numeric' => ':attribute은(는) :min에서 :max 사이여야 합니다.',
'string' => ':attribute은(는) :min자에서 :max자 사이여야 합니다.',
],
'boolean' => ':attribute은(는) 참 또는 거짓이어야 합니다.',
'can' => ':attribute에 허용되지 않은 값이 포함되어 있습니다.',
'confirmed' => ':attribute 확인이 일치하지 않습니다.',
'contains' => ':attribute에 필수 값이 없습니다.',
'current_password' => '비밀번호가 올바르지 않습니다.',
'date' => ':attribute은(는) 유효한 날짜여야 합니다.',
'date_equals' => ':attribute은(는) :date와 같은 날짜여야 합니다.',
'date_format' => ':attribute은(는) :format 형식이어야 합니다.',
'decimal' => ':attribute은(는) :decimal자리 소수여야 합니다.',
'declined' => ':attribute을(를) 거부해야 합니다.',
'declined_if' => ':other이(가) :value일 때 :attribute을(를) 거부해야 합니다.',
'different' => ':attribute과(와) :other은(는) 달라야 합니다.',
'digits' => ':attribute은(는) :digits자리 숫자여야 합니다.',
'digits_between' => ':attribute은(는) :min자리에서 :max자리 숫자여야 합니다.',
'dimensions' => ':attribute 이미지 크기가 유효하지 않습니다.',
'distinct' => ':attribute에 중복된 값이 있습니다.',
'doesnt_contain' => ':attribute에 다음을 포함할 수 없습니다: :values.',
'doesnt_end_with' => ':attribute은(는) 다음으로 끝나면 안 됩니다: :values.',
'doesnt_start_with' => ':attribute은(는) 다음으로 시작하면 안 됩니다: :values.',
'email' => ':attribute은(는) 유효한 이메일 주소여야 합니다.',
'encoding' => ':attribute은(는) :encoding으로 인코딩되어야 합니다.',
'ends_with' => ':attribute은(는) 다음 중 하나로 끝나야 합니다: :values.',
'enum' => '선택한 :attribute이(가) 유효하지 않습니다.',
'exists' => '선택한 :attribute이(가) 유효하지 않습니다.',
'extensions' => ':attribute은(는) 다음 확장자 중 하나여야 합니다: :values.',
'file' => ':attribute은(는) 파일이어야 합니다.',
'filled' => ':attribute에는 값이 있어야 합니다.',
'gt' => [
'array' => ':attribute은(는) :value개보다 많아야 합니다.',
'file' => ':attribute은(는) :value KB보다 커야 합니다.',
'numeric' => ':attribute은(는) :value보다 커야 합니다.',
'string' => ':attribute은(는) :value자보다 많아야 합니다.',
],
'gte' => [
'array' => ':attribute은(는) :value개 이상이어야 합니다.',
'file' => ':attribute은(는) :value KB 이상이어야 합니다.',
'numeric' => ':attribute은(는) :value 이상이어야 합니다.',
'string' => ':attribute은(는) :value자 이상이어야 합니다.',
],
'hex_color' => ':attribute은(는) 유효한 16진수 색상이어야 합니다.',
'image' => ':attribute은(는) 이미지여야 합니다.',
'in' => '선택한 :attribute이(가) 유효하지 않습니다.',
'in_array' => ':attribute은(는) :other에 존재해야 합니다.',
'in_array_keys' => ':attribute은(는) 다음 키 중 하나를 포함해야 합니다: :values.',
'integer' => ':attribute은(는) 정수여야 합니다.',
'ip' => ':attribute은(는) 유효한 IP 주소여야 합니다.',
'ipv4' => ':attribute은(는) 유효한 IPv4 주소여야 합니다.',
'ipv6' => ':attribute은(는) 유효한 IPv6 주소여야 합니다.',
'json' => ':attribute은(는) 유효한 JSON 문자열이어야 합니다.',
'list' => ':attribute은(는) 목록이어야 합니다.',
'lowercase' => ':attribute은(는) 소문자여야 합니다.',
'lt' => [
'array' => ':attribute은(는) :value개 미만이어야 합니다.',
'file' => ':attribute은(는) :value KB 미만이어야 합니다.',
'numeric' => ':attribute은(는) :value 미만이어야 합니다.',
'string' => ':attribute은(는) :value자 미만이어야 합니다.',
],
'lte' => [
'array' => ':attribute은(는) :value개 이하여야 합니다.',
'file' => ':attribute은(는) :value KB 이하여야 합니다.',
'numeric' => ':attribute은(는) :value 이하여야 합니다.',
'string' => ':attribute은(는) :value자 이하여야 합니다.',
],
'mac_address' => ':attribute은(는) 유효한 MAC 주소여야 합니다.',
'max' => [
'array' => ':attribute은(는) :max개 이하여야 합니다.',
'file' => ':attribute은(는) :max KB 이하여야 합니다.',
'numeric' => ':attribute은(는) :max 이하여야 합니다.',
'string' => ':attribute은(는) :max자 이하여야 합니다.',
],
'max_digits' => ':attribute은(는) :max자리 이하여야 합니다.',
'mimes' => ':attribute은(는) :values 유형의 파일이어야 합니다.',
'mimetypes' => ':attribute은(는) :values 유형의 파일이어야 합니다.',
'min' => [
'array' => ':attribute은(는) 최소 :min개여야 합니다.',
'file' => ':attribute은(는) 최소 :min KB여야 합니다.',
'numeric' => ':attribute은(는) 최소 :min이어야 합니다.',
'string' => ':attribute은(는) 최소 :min자여야 합니다.',
],
'min_digits' => ':attribute은(는) 최소 :min자리여야 합니다.',
'missing' => ':attribute은(는) 없어야 합니다.',
'missing_if' => ':other이(가) :value일 때 :attribute은(는) 없어야 합니다.',
'missing_unless' => ':other이(가) :value가 아니면 :attribute은(는) 없어야 합니다.',
'missing_with' => ':values이(가) 있으면 :attribute은(는) 없어야 합니다.',
'missing_with_all' => ':values이(가) 모두 있으면 :attribute은(는) 없어야 합니다.',
'multiple_of' => ':attribute은(는) :value의 배수여야 합니다.',
'not_in' => '선택한 :attribute이(가) 유효하지 않습니다.',
'not_regex' => ':attribute 형식이 유효하지 않습니다.',
'numeric' => ':attribute은(는) 숫자여야 합니다.',
'password' => [
'letters' => ':attribute은(는) 최소 한 개의 문자를 포함해야 합니다.',
'mixed' => ':attribute은(는) 최소 한 개의 대문자와 한 개의 소문자를 포함해야 합니다.',
'numbers' => ':attribute은(는) 최소 한 개의 숫자를 포함해야 합니다.',
'symbols' => ':attribute은(는) 최소 한 개의 기호를 포함해야 합니다.',
'uncompromised' => ':attribute이(가) 데이터 유출에서 발견되었습니다. 다른 :attribute을(를) 선택하세요.',
],
'present' => ':attribute이(가) 있어야 합니다.',
'present_if' => ':other이(가) :value일 때 :attribute이(가) 있어야 합니다.',
'present_unless' => ':other이(가) :value가 아니면 :attribute이(가) 있어야 합니다.',
'present_with' => ':values이(가) 있으면 :attribute이(가) 있어야 합니다.',
'present_with_all' => ':values이(가) 모두 있으면 :attribute이(가) 있어야 합니다.',
'prohibited' => ':attribute은(는) 금지되어 있습니다.',
'prohibited_if' => ':other이(가) :value일 때 :attribute은(는) 금지되어 있습니다.',
'prohibited_if_accepted' => ':other이(가) 수락되면 :attribute은(는) 금지되어 있습니다.',
'prohibited_if_declined' => ':other이(가) 거부되면 :attribute은(는) 금지되어 있습니다.',
'prohibited_unless' => ':other이(가) :values에 없으면 :attribute은(는) 금지되어 있습니다.',
'prohibits' => ':attribute이(가) 있으면 :other은(는) 금지되어 있습니다.',
'regex' => ':attribute 형식이 유효하지 않습니다.',
'required' => ':attribute은(는) 필수입니다.',
'required_array_keys' => ':attribute은(는) 다음 키를 포함해야 합니다: :values.',
'required_if' => ':other이(가) :value일 때 :attribute은(는) 필수입니다.',
'required_if_accepted' => ':other이(가) 수락되면 :attribute은(는) 필수입니다.',
'required_if_declined' => ':other이(가) 거부되면 :attribute은(는) 필수입니다.',
'required_unless' => ':other이(가) :values에 없으면 :attribute은(는) 필수입니다.',
'required_with' => ':values이(가) 있으면 :attribute은(는) 필수입니다.',
'required_with_all' => ':values이(가) 모두 있으면 :attribute은(는) 필수입니다.',
'required_without' => ':values이(가) 없으면 :attribute은(는) 필수입니다.',
'required_without_all' => ':values이(가) 모두 없으면 :attribute은(는) 필수입니다.',
'same' => ':attribute과(와) :other은(는) 일치해야 합니다.',
'size' => [
'array' => ':attribute은(는) :size개여야 합니다.',
'file' => ':attribute은(는) :size KB여야 합니다.',
'numeric' => ':attribute은(는) :size여야 합니다.',
'string' => ':attribute은(는) :size자여야 합니다.',
],
'starts_with' => ':attribute은(는) 다음 중 하나로 시작해야 합니다: :values.',
'string' => ':attribute은(는) 문자열이어야 합니다.',
'timezone' => ':attribute은(는) 유효한 시간대여야 합니다.',
'unique' => ':attribute은(는) 이미 사용 중입니다.',
'uploaded' => ':attribute 업로드에 실패했습니다.',
'uppercase' => ':attribute은(는) 대문자여야 합니다.',
'url' => ':attribute은(는) 유효한 URL이어야 합니다.',
'ulid' => ':attribute은(는) 유효한 ULID여야 합니다.',
'uuid' => ':attribute은(는) 유효한 UUID여야 합니다.',
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
'attributes' => [],
];
@@ -0,0 +1,170 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Mensagens de validação
|--------------------------------------------------------------------------
*/
'accepted' => 'O campo :attribute deve ser aceito.',
'accepted_if' => 'O campo :attribute deve ser aceito quando :other for :value.',
'active_url' => 'O campo :attribute deve ser uma URL válida.',
'after' => 'O campo :attribute deve ser uma data posterior a :date.',
'after_or_equal' => 'O campo :attribute deve ser uma data posterior ou igual a :date.',
'alpha' => 'O campo :attribute deve conter apenas letras.',
'alpha_dash' => 'O campo :attribute deve conter apenas letras, números, traços e sublinhados.',
'alpha_num' => 'O campo :attribute deve conter apenas letras e números.',
'any_of' => 'O campo :attribute é inválido.',
'array' => 'O campo :attribute deve ser um array.',
'ascii' => 'O campo :attribute deve conter apenas caracteres alfanuméricos e símbolos de um byte.',
'before' => 'O campo :attribute deve ser uma data anterior a :date.',
'before_or_equal' => 'O campo :attribute deve ser uma data anterior ou igual a :date.',
'between' => [
'array' => 'O campo :attribute deve ter entre :min e :max itens.',
'file' => 'O campo :attribute deve ter entre :min e :max kilobytes.',
'numeric' => 'O campo :attribute deve estar entre :min e :max.',
'string' => 'O campo :attribute deve ter entre :min e :max caracteres.',
],
'boolean' => 'O campo :attribute deve ser verdadeiro ou falso.',
'can' => 'O campo :attribute contém um valor não autorizado.',
'confirmed' => 'A confirmação do campo :attribute não corresponde.',
'contains' => 'O campo :attribute não contém um valor obrigatório.',
'current_password' => 'A senha está incorreta.',
'date' => 'O campo :attribute deve ser uma data válida.',
'date_equals' => 'O campo :attribute deve ser uma data igual a :date.',
'date_format' => 'O campo :attribute deve corresponder ao formato :format.',
'decimal' => 'O campo :attribute deve ter :decimal casas decimais.',
'declined' => 'O campo :attribute deve ser recusado.',
'declined_if' => 'O campo :attribute deve ser recusado quando :other for :value.',
'different' => 'Os campos :attribute e :other devem ser diferentes.',
'digits' => 'O campo :attribute deve ter :digits dígitos.',
'digits_between' => 'O campo :attribute deve ter entre :min e :max dígitos.',
'dimensions' => 'O campo :attribute tem dimensões de imagem inválidas.',
'distinct' => 'O campo :attribute tem um valor duplicado.',
'doesnt_contain' => 'O campo :attribute não deve conter: :values.',
'doesnt_end_with' => 'O campo :attribute não deve terminar com: :values.',
'doesnt_start_with' => 'O campo :attribute não deve começar com: :values.',
'email' => 'O campo :attribute deve ser um endereço de e-mail válido.',
'encoding' => 'O campo :attribute deve estar codificado em :encoding.',
'ends_with' => 'O campo :attribute deve terminar com: :values.',
'enum' => 'O :attribute selecionado é inválido.',
'exists' => 'O :attribute selecionado é inválido.',
'extensions' => 'O campo :attribute deve ter uma das seguintes extensões: :values.',
'file' => 'O campo :attribute deve ser um arquivo.',
'filled' => 'O campo :attribute deve ter um valor.',
'gt' => [
'array' => 'O campo :attribute deve ter mais de :value itens.',
'file' => 'O campo :attribute deve ser maior que :value kilobytes.',
'numeric' => 'O campo :attribute deve ser maior que :value.',
'string' => 'O campo :attribute deve ter mais de :value caracteres.',
],
'gte' => [
'array' => 'O campo :attribute deve ter :value itens ou mais.',
'file' => 'O campo :attribute deve ser maior ou igual a :value kilobytes.',
'numeric' => 'O campo :attribute deve ser maior ou igual a :value.',
'string' => 'O campo :attribute deve ter :value caracteres ou mais.',
],
'hex_color' => 'O campo :attribute deve ser uma cor hexadecimal válida.',
'image' => 'O campo :attribute deve ser uma imagem.',
'in' => 'O :attribute selecionado é inválido.',
'in_array' => 'O campo :attribute deve existir em :other.',
'in_array_keys' => 'O campo :attribute deve conter pelo menos uma das seguintes chaves: :values.',
'integer' => 'O campo :attribute deve ser um número inteiro.',
'ip' => 'O campo :attribute deve ser um endereço IP válido.',
'ipv4' => 'O campo :attribute deve ser um endereço IPv4 válido.',
'ipv6' => 'O campo :attribute deve ser um endereço IPv6 válido.',
'json' => 'O campo :attribute deve ser uma string JSON válida.',
'list' => 'O campo :attribute deve ser uma lista.',
'lowercase' => 'O campo :attribute deve estar em minúsculas.',
'lt' => [
'array' => 'O campo :attribute deve ter menos de :value itens.',
'file' => 'O campo :attribute deve ser menor que :value kilobytes.',
'numeric' => 'O campo :attribute deve ser menor que :value.',
'string' => 'O campo :attribute deve ter menos de :value caracteres.',
],
'lte' => [
'array' => 'O campo :attribute não deve ter mais de :value itens.',
'file' => 'O campo :attribute deve ser menor ou igual a :value kilobytes.',
'numeric' => 'O campo :attribute deve ser menor ou igual a :value.',
'string' => 'O campo :attribute deve ter :value caracteres ou menos.',
],
'mac_address' => 'O campo :attribute deve ser um endereço MAC válido.',
'max' => [
'array' => 'O campo :attribute não deve ter mais de :max itens.',
'file' => 'O campo :attribute não deve ser maior que :max kilobytes.',
'numeric' => 'O campo :attribute não deve ser maior que :max.',
'string' => 'O campo :attribute não deve ter mais de :max caracteres.',
],
'max_digits' => 'O campo :attribute não deve ter mais de :max dígitos.',
'mimes' => 'O campo :attribute deve ser um arquivo do tipo: :values.',
'mimetypes' => 'O campo :attribute deve ser um arquivo do tipo: :values.',
'min' => [
'array' => 'O campo :attribute deve ter pelo menos :min itens.',
'file' => 'O campo :attribute deve ter pelo menos :min kilobytes.',
'numeric' => 'O campo :attribute deve ser pelo menos :min.',
'string' => 'O campo :attribute deve ter pelo menos :min caracteres.',
],
'min_digits' => 'O campo :attribute deve ter pelo menos :min dígitos.',
'missing' => 'O campo :attribute deve estar ausente.',
'missing_if' => 'O campo :attribute deve estar ausente quando :other for :value.',
'missing_unless' => 'O campo :attribute deve estar ausente a menos que :other seja :value.',
'missing_with' => 'O campo :attribute deve estar ausente quando :values estiver presente.',
'missing_with_all' => 'O campo :attribute deve estar ausente quando :values estiverem presentes.',
'multiple_of' => 'O campo :attribute deve ser um múltiplo de :value.',
'not_in' => 'O :attribute selecionado é inválido.',
'not_regex' => 'O formato do campo :attribute é inválido.',
'numeric' => 'O campo :attribute deve ser um número.',
'password' => [
'letters' => 'O campo :attribute deve conter pelo menos uma letra.',
'mixed' => 'O campo :attribute deve conter pelo menos uma letra maiúscula e uma minúscula.',
'numbers' => 'O campo :attribute deve conter pelo menos um número.',
'symbols' => 'O campo :attribute deve conter pelo menos um símbolo.',
'uncompromised' => 'O :attribute fornecido apareceu em um vazamento de dados. Por favor, escolha um :attribute diferente.',
],
'present' => 'O campo :attribute deve estar presente.',
'present_if' => 'O campo :attribute deve estar presente quando :other for :value.',
'present_unless' => 'O campo :attribute deve estar presente a menos que :other seja :value.',
'present_with' => 'O campo :attribute deve estar presente quando :values estiver presente.',
'present_with_all' => 'O campo :attribute deve estar presente quando :values estiverem presentes.',
'prohibited' => 'O campo :attribute é proibido.',
'prohibited_if' => 'O campo :attribute é proibido quando :other for :value.',
'prohibited_if_accepted' => 'O campo :attribute é proibido quando :other for aceito.',
'prohibited_if_declined' => 'O campo :attribute é proibido quando :other for recusado.',
'prohibited_unless' => 'O campo :attribute é proibido a menos que :other esteja em :values.',
'prohibits' => 'O campo :attribute proíbe que :other esteja presente.',
'regex' => 'O formato do campo :attribute é inválido.',
'required' => 'O campo :attribute é obrigatório.',
'required_array_keys' => 'O campo :attribute deve conter entradas para: :values.',
'required_if' => 'O campo :attribute é obrigatório quando :other for :value.',
'required_if_accepted' => 'O campo :attribute é obrigatório quando :other for aceito.',
'required_if_declined' => 'O campo :attribute é obrigatório quando :other for recusado.',
'required_unless' => 'O campo :attribute é obrigatório a menos que :other esteja em :values.',
'required_with' => 'O campo :attribute é obrigatório quando :values estiver presente.',
'required_with_all' => 'O campo :attribute é obrigatório quando :values estiverem presentes.',
'required_without' => 'O campo :attribute é obrigatório quando :values não estiver presente.',
'required_without_all' => 'O campo :attribute é obrigatório quando nenhum de :values estiver presente.',
'same' => 'Os campos :attribute e :other devem corresponder.',
'size' => [
'array' => 'O campo :attribute deve conter :size itens.',
'file' => 'O campo :attribute deve ter :size kilobytes.',
'numeric' => 'O campo :attribute deve ser :size.',
'string' => 'O campo :attribute deve ter :size caracteres.',
],
'starts_with' => 'O campo :attribute deve começar com: :values.',
'string' => 'O campo :attribute deve ser uma string.',
'timezone' => 'O campo :attribute deve ser um fuso horário válido.',
'unique' => 'O :attribute já foi utilizado.',
'uploaded' => 'O upload do :attribute falhou.',
'uppercase' => 'O campo :attribute deve estar em maiúsculas.',
'url' => 'O campo :attribute deve ser uma URL válida.',
'ulid' => 'O campo :attribute deve ser um ULID válido.',
'uuid' => 'O campo :attribute deve ser um UUID válido.',
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
'attributes' => [],
];
@@ -0,0 +1,170 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Сообщения валидации
|--------------------------------------------------------------------------
*/
'accepted' => 'Поле :attribute должно быть принято.',
'accepted_if' => 'Поле :attribute должно быть принято, когда :other равно :value.',
'active_url' => 'Поле :attribute должно быть действительным URL.',
'after' => 'Поле :attribute должно быть датой после :date.',
'after_or_equal' => 'Поле :attribute должно быть датой после или равной :date.',
'alpha' => 'Поле :attribute должно содержать только буквы.',
'alpha_dash' => 'Поле :attribute должно содержать только буквы, цифры, дефисы и подчёркивания.',
'alpha_num' => 'Поле :attribute должно содержать только буквы и цифры.',
'any_of' => 'Поле :attribute недействительно.',
'array' => 'Поле :attribute должно быть массивом.',
'ascii' => 'Поле :attribute должно содержать только однобайтовые буквенно-цифровые символы и символы.',
'before' => 'Поле :attribute должно быть датой до :date.',
'before_or_equal' => 'Поле :attribute должно быть датой до или равной :date.',
'between' => [
'array' => 'Поле :attribute должно содержать от :min до :max элементов.',
'file' => 'Поле :attribute должно быть от :min до :max килобайт.',
'numeric' => 'Поле :attribute должно быть от :min до :max.',
'string' => 'Поле :attribute должно содержать от :min до :max символов.',
],
'boolean' => 'Поле :attribute должно быть истиной или ложью.',
'can' => 'Поле :attribute содержит неразрешённое значение.',
'confirmed' => 'Подтверждение поля :attribute не совпадает.',
'contains' => 'Поле :attribute не содержит обязательного значения.',
'current_password' => 'Пароль неверный.',
'date' => 'Поле :attribute должно быть действительной датой.',
'date_equals' => 'Поле :attribute должно быть датой, равной :date.',
'date_format' => 'Поле :attribute должно соответствовать формату :format.',
'decimal' => 'Поле :attribute должно иметь :decimal десятичных знаков.',
'declined' => 'Поле :attribute должно быть отклонено.',
'declined_if' => 'Поле :attribute должно быть отклонено, когда :other равно :value.',
'different' => 'Поля :attribute и :other должны различаться.',
'digits' => 'Поле :attribute должно содержать :digits цифр.',
'digits_between' => 'Поле :attribute должно содержать от :min до :max цифр.',
'dimensions' => 'Поле :attribute имеет недействительные размеры изображения.',
'distinct' => 'Поле :attribute содержит повторяющееся значение.',
'doesnt_contain' => 'Поле :attribute не должно содержать: :values.',
'doesnt_end_with' => 'Поле :attribute не должно заканчиваться на: :values.',
'doesnt_start_with' => 'Поле :attribute не должно начинаться с: :values.',
'email' => 'Поле :attribute должно быть действительным адресом электронной почты.',
'encoding' => 'Поле :attribute должно быть закодировано в :encoding.',
'ends_with' => 'Поле :attribute должно заканчиваться на: :values.',
'enum' => 'Выбранное :attribute недействительно.',
'exists' => 'Выбранное :attribute недействительно.',
'extensions' => 'Поле :attribute должно иметь одно из следующих расширений: :values.',
'file' => 'Поле :attribute должно быть файлом.',
'filled' => 'Поле :attribute должно иметь значение.',
'gt' => [
'array' => 'Поле :attribute должно содержать более :value элементов.',
'file' => 'Поле :attribute должно быть больше :value килобайт.',
'numeric' => 'Поле :attribute должно быть больше :value.',
'string' => 'Поле :attribute должно содержать более :value символов.',
],
'gte' => [
'array' => 'Поле :attribute должно содержать :value элементов или более.',
'file' => 'Поле :attribute должно быть больше или равно :value килобайт.',
'numeric' => 'Поле :attribute должно быть больше или равно :value.',
'string' => 'Поле :attribute должно содержать :value символов или более.',
],
'hex_color' => 'Поле :attribute должно быть действительным шестнадцатеричным цветом.',
'image' => 'Поле :attribute должно быть изображением.',
'in' => 'Выбранное :attribute недействительно.',
'in_array' => 'Поле :attribute должно существовать в :other.',
'in_array_keys' => 'Поле :attribute должно содержать хотя бы один из следующих ключей: :values.',
'integer' => 'Поле :attribute должно быть целым числом.',
'ip' => 'Поле :attribute должно быть действительным IP-адресом.',
'ipv4' => 'Поле :attribute должно быть действительным IPv4-адресом.',
'ipv6' => 'Поле :attribute должно быть действительным IPv6-адресом.',
'json' => 'Поле :attribute должно быть действительной JSON-строкой.',
'list' => 'Поле :attribute должно быть списком.',
'lowercase' => 'Поле :attribute должно быть в нижнем регистре.',
'lt' => [
'array' => 'Поле :attribute должно содержать менее :value элементов.',
'file' => 'Поле :attribute должно быть меньше :value килобайт.',
'numeric' => 'Поле :attribute должно быть меньше :value.',
'string' => 'Поле :attribute должно содержать менее :value символов.',
],
'lte' => [
'array' => 'Поле :attribute не должно содержать более :value элементов.',
'file' => 'Поле :attribute должно быть меньше или равно :value килобайт.',
'numeric' => 'Поле :attribute должно быть меньше или равно :value.',
'string' => 'Поле :attribute должно содержать :value символов или менее.',
],
'mac_address' => 'Поле :attribute должно быть действительным MAC-адресом.',
'max' => [
'array' => 'Поле :attribute не должно содержать более :max элементов.',
'file' => 'Поле :attribute не должно быть больше :max килобайт.',
'numeric' => 'Поле :attribute не должно быть больше :max.',
'string' => 'Поле :attribute не должно содержать более :max символов.',
],
'max_digits' => 'Поле :attribute не должно содержать более :max цифр.',
'mimes' => 'Поле :attribute должно быть файлом типа: :values.',
'mimetypes' => 'Поле :attribute должно быть файлом типа: :values.',
'min' => [
'array' => 'Поле :attribute должно содержать не менее :min элементов.',
'file' => 'Поле :attribute должно быть не менее :min килобайт.',
'numeric' => 'Поле :attribute должно быть не менее :min.',
'string' => 'Поле :attribute должно содержать не менее :min символов.',
],
'min_digits' => 'Поле :attribute должно содержать не менее :min цифр.',
'missing' => 'Поле :attribute должно отсутствовать.',
'missing_if' => 'Поле :attribute должно отсутствовать, когда :other равно :value.',
'missing_unless' => 'Поле :attribute должно отсутствовать, если :other не равно :value.',
'missing_with' => 'Поле :attribute должно отсутствовать, когда присутствует :values.',
'missing_with_all' => 'Поле :attribute должно отсутствовать, когда присутствуют :values.',
'multiple_of' => 'Поле :attribute должно быть кратным :value.',
'not_in' => 'Выбранное :attribute недействительно.',
'not_regex' => 'Формат поля :attribute недействителен.',
'numeric' => 'Поле :attribute должно быть числом.',
'password' => [
'letters' => 'Поле :attribute должно содержать хотя бы одну букву.',
'mixed' => 'Поле :attribute должно содержать хотя бы одну заглавную и одну строчную букву.',
'numbers' => 'Поле :attribute должно содержать хотя бы одну цифру.',
'symbols' => 'Поле :attribute должно содержать хотя бы один символ.',
'uncompromised' => 'Указанное :attribute появилось в утечке данных. Пожалуйста, выберите другое :attribute.',
],
'present' => 'Поле :attribute должно присутствовать.',
'present_if' => 'Поле :attribute должно присутствовать, когда :other равно :value.',
'present_unless' => 'Поле :attribute должно присутствовать, если :other не равно :value.',
'present_with' => 'Поле :attribute должно присутствовать, когда присутствует :values.',
'present_with_all' => 'Поле :attribute должно присутствовать, когда присутствуют :values.',
'prohibited' => 'Поле :attribute запрещено.',
'prohibited_if' => 'Поле :attribute запрещено, когда :other равно :value.',
'prohibited_if_accepted' => 'Поле :attribute запрещено, когда :other принято.',
'prohibited_if_declined' => 'Поле :attribute запрещено, когда :other отклонено.',
'prohibited_unless' => 'Поле :attribute запрещено, если :other не в :values.',
'prohibits' => 'Поле :attribute запрещает присутствие :other.',
'regex' => 'Формат поля :attribute недействителен.',
'required' => 'Поле :attribute обязательно.',
'required_array_keys' => 'Поле :attribute должно содержать записи для: :values.',
'required_if' => 'Поле :attribute обязательно, когда :other равно :value.',
'required_if_accepted' => 'Поле :attribute обязательно, когда :other принято.',
'required_if_declined' => 'Поле :attribute обязательно, когда :other отклонено.',
'required_unless' => 'Поле :attribute обязательно, если :other не в :values.',
'required_with' => 'Поле :attribute обязательно, когда присутствует :values.',
'required_with_all' => 'Поле :attribute обязательно, когда присутствуют :values.',
'required_without' => 'Поле :attribute обязательно, когда :values отсутствует.',
'required_without_all' => 'Поле :attribute обязательно, когда ни одно из :values не присутствует.',
'same' => 'Поля :attribute и :other должны совпадать.',
'size' => [
'array' => 'Поле :attribute должно содержать :size элементов.',
'file' => 'Поле :attribute должно быть :size килобайт.',
'numeric' => 'Поле :attribute должно быть :size.',
'string' => 'Поле :attribute должно содержать :size символов.',
],
'starts_with' => 'Поле :attribute должно начинаться с: :values.',
'string' => 'Поле :attribute должно быть строкой.',
'timezone' => 'Поле :attribute должно быть действительным часовым поясом.',
'unique' => 'Такое :attribute уже существует.',
'uploaded' => 'Загрузка :attribute не удалась.',
'uppercase' => 'Поле :attribute должно быть в верхнем регистре.',
'url' => 'Поле :attribute должно быть действительным URL.',
'ulid' => 'Поле :attribute должно быть действительным ULID.',
'uuid' => 'Поле :attribute должно быть действительным UUID.',
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
'attributes' => [],
];
@@ -0,0 +1,170 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| ข้อความการตรวจสอบ
|--------------------------------------------------------------------------
*/
'accepted' => 'ฟิลด์ :attribute ต้องได้รับการยอมรับ',
'accepted_if' => 'ฟิลด์ :attribute ต้องได้รับการยอมรับเมื่อ :other เป็น :value',
'active_url' => 'ฟิลด์ :attribute ต้องเป็น URL ที่ถูกต้อง',
'after' => 'ฟิลด์ :attribute ต้องเป็นวันที่หลังจาก :date',
'after_or_equal' => 'ฟิลด์ :attribute ต้องเป็นวันที่หลังหรือเท่ากับ :date',
'alpha' => 'ฟิลด์ :attribute ต้องมีเฉพาะตัวอักษรเท่านั้น',
'alpha_dash' => 'ฟิลด์ :attribute ต้องมีเฉพาะตัวอักษร ตัวเลข ขีด และขีดล่างเท่านั้น',
'alpha_num' => 'ฟิลด์ :attribute ต้องมีเฉพาะตัวอักษรและตัวเลขเท่านั้น',
'any_of' => 'ฟิลด์ :attribute ไม่ถูกต้อง',
'array' => 'ฟิลด์ :attribute ต้องเป็นอาร์เรย์',
'ascii' => 'ฟิลด์ :attribute ต้องมีเฉพาะอักขระตัวเลขและสัญลักษณ์แบบไบต์เดียวเท่านั้น',
'before' => 'ฟิลด์ :attribute ต้องเป็นวันที่ก่อน :date',
'before_or_equal' => 'ฟิลด์ :attribute ต้องเป็นวันที่ก่อนหรือเท่ากับ :date',
'between' => [
'array' => 'ฟิลด์ :attribute ต้องมีระหว่าง :min ถึง :max รายการ',
'file' => 'ฟิลด์ :attribute ต้องมีขนาดระหว่าง :min ถึง :max กิโลไบต์',
'numeric' => 'ฟิลด์ :attribute ต้องอยู่ระหว่าง :min ถึง :max',
'string' => 'ฟิลด์ :attribute ต้องมีความยาวระหว่าง :min ถึง :max ตัวอักษร',
],
'boolean' => 'ฟิลด์ :attribute ต้องเป็นจริงหรือเท็จ',
'can' => 'ฟิลด์ :attribute มีค่าที่ไม่ได้รับอนุญาต',
'confirmed' => 'การยืนยันฟิลด์ :attribute ไม่ตรงกัน',
'contains' => 'ฟิลด์ :attribute ไม่มีค่าที่จำเป็น',
'current_password' => 'รหัสผ่านไม่ถูกต้อง',
'date' => 'ฟิลด์ :attribute ต้องเป็นวันที่ที่ถูกต้อง',
'date_equals' => 'ฟิลด์ :attribute ต้องเป็นวันที่เท่ากับ :date',
'date_format' => 'ฟิลด์ :attribute ต้องตรงกับรูปแบบ :format',
'decimal' => 'ฟิลด์ :attribute ต้องมีทศนิยม :decimal ตำแหน่ง',
'declined' => 'ฟิลด์ :attribute ต้องถูกปฏิเสธ',
'declined_if' => 'ฟิลด์ :attribute ต้องถูกปฏิเสธเมื่อ :other เป็น :value',
'different' => 'ฟิลด์ :attribute และ :other ต้องแตกต่างกัน',
'digits' => 'ฟิลด์ :attribute ต้องมี :digits หลัก',
'digits_between' => 'ฟิลด์ :attribute ต้องมีระหว่าง :min ถึง :max หลัก',
'dimensions' => 'ฟิลด์ :attribute มีขนาดรูปภาพไม่ถูกต้อง',
'distinct' => 'ฟิลด์ :attribute มีค่าซ้ำ',
'doesnt_contain' => 'ฟิลด์ :attribute ต้องไม่มี: :values',
'doesnt_end_with' => 'ฟิลด์ :attribute ต้องไม่ลงท้ายด้วย: :values',
'doesnt_start_with' => 'ฟิลด์ :attribute ต้องไม่ขึ้นต้นด้วย: :values',
'email' => 'ฟิลด์ :attribute ต้องเป็นที่อยู่อีเมลที่ถูกต้อง',
'encoding' => 'ฟิลด์ :attribute ต้องเข้ารหัสด้วย :encoding',
'ends_with' => 'ฟิลด์ :attribute ต้องลงท้ายด้วย: :values',
'enum' => ':attribute ที่เลือกไม่ถูกต้อง',
'exists' => ':attribute ที่เลือกไม่ถูกต้อง',
'extensions' => 'ฟิลด์ :attribute ต้องมีนามสกุลใดนามสกุลหนึ่งต่อไปนี้: :values',
'file' => 'ฟิลด์ :attribute ต้องเป็นไฟล์',
'filled' => 'ฟิลด์ :attribute ต้องมีค่า',
'gt' => [
'array' => 'ฟิลด์ :attribute ต้องมีมากกว่า :value รายการ',
'file' => 'ฟิลด์ :attribute ต้องใหญ่กว่า :value กิโลไบต์',
'numeric' => 'ฟิลด์ :attribute ต้องมากกว่า :value',
'string' => 'ฟิลด์ :attribute ต้องยาวกว่า :value ตัวอักษร',
],
'gte' => [
'array' => 'ฟิลด์ :attribute ต้องมี :value รายการขึ้นไป',
'file' => 'ฟิลด์ :attribute ต้องใหญ่กว่าหรือเท่ากับ :value กิโลไบต์',
'numeric' => 'ฟิลด์ :attribute ต้องมากกว่าหรือเท่ากับ :value',
'string' => 'ฟิลด์ :attribute ต้องยาว :value ตัวอักษรขึ้นไป',
],
'hex_color' => 'ฟิลด์ :attribute ต้องเป็นสีฐานสิบหกที่ถูกต้อง',
'image' => 'ฟิลด์ :attribute ต้องเป็นรูปภาพ',
'in' => ':attribute ที่เลือกไม่ถูกต้อง',
'in_array' => 'ฟิลด์ :attribute ต้องมีอยู่ใน :other',
'in_array_keys' => 'ฟิลด์ :attribute ต้องมีคีย์อย่างน้อยหนึ่งรายการต่อไปนี้: :values',
'integer' => 'ฟิลด์ :attribute ต้องเป็นจำนวนเต็ม',
'ip' => 'ฟิลด์ :attribute ต้องเป็นที่อยู่ IP ที่ถูกต้อง',
'ipv4' => 'ฟิลด์ :attribute ต้องเป็นที่อยู่ IPv4 ที่ถูกต้อง',
'ipv6' => 'ฟิลด์ :attribute ต้องเป็นที่อยู่ IPv6 ที่ถูกต้อง',
'json' => 'ฟิลด์ :attribute ต้องเป็นสตริง JSON ที่ถูกต้อง',
'list' => 'ฟิลด์ :attribute ต้องเป็นรายการ',
'lowercase' => 'ฟิลด์ :attribute ต้องเป็นตัวพิมพ์เล็ก',
'lt' => [
'array' => 'ฟิลด์ :attribute ต้องมีน้อยกว่า :value รายการ',
'file' => 'ฟิลด์ :attribute ต้องเล็กกว่า :value กิโลไบต์',
'numeric' => 'ฟิลด์ :attribute ต้องน้อยกว่า :value',
'string' => 'ฟิลด์ :attribute ต้องสั้นกว่า :value ตัวอักษร',
],
'lte' => [
'array' => 'ฟิลด์ :attribute ต้องไม่เกิน :value รายการ',
'file' => 'ฟิลด์ :attribute ต้องเล็กกว่าหรือเท่ากับ :value กิโลไบต์',
'numeric' => 'ฟิลด์ :attribute ต้องน้อยกว่าหรือเท่ากับ :value',
'string' => 'ฟิลด์ :attribute ต้องไม่เกิน :value ตัวอักษร',
],
'mac_address' => 'ฟิลด์ :attribute ต้องเป็นที่อยู่ MAC ที่ถูกต้อง',
'max' => [
'array' => 'ฟิลด์ :attribute ต้องไม่เกิน :max รายการ',
'file' => 'ฟิลด์ :attribute ต้องไม่เกิน :max กิโลไบต์',
'numeric' => 'ฟิลด์ :attribute ต้องไม่เกิน :max',
'string' => 'ฟิลด์ :attribute ต้องไม่เกิน :max ตัวอักษร',
],
'max_digits' => 'ฟิลด์ :attribute ต้องไม่เกิน :max หลัก',
'mimes' => 'ฟิลด์ :attribute ต้องเป็นไฟล์ประเภท: :values',
'mimetypes' => 'ฟิลด์ :attribute ต้องเป็นไฟล์ประเภท: :values',
'min' => [
'array' => 'ฟิลด์ :attribute ต้องมีอย่างน้อย :min รายการ',
'file' => 'ฟิลด์ :attribute ต้องมีอย่างน้อย :min กิโลไบต์',
'numeric' => 'ฟิลด์ :attribute ต้องมีอย่างน้อย :min',
'string' => 'ฟิลด์ :attribute ต้องมีอย่างน้อย :min ตัวอักษร',
],
'min_digits' => 'ฟิลด์ :attribute ต้องมีอย่างน้อย :min หลัก',
'missing' => 'ฟิลด์ :attribute ต้องไม่มี',
'missing_if' => 'ฟิลด์ :attribute ต้องไม่มีเมื่อ :other เป็น :value',
'missing_unless' => 'ฟิลด์ :attribute ต้องไม่มีเว้นแต่ :other เป็น :value',
'missing_with' => 'ฟิลด์ :attribute ต้องไม่มีเมื่อ :values มีอยู่',
'missing_with_all' => 'ฟิลด์ :attribute ต้องไม่มีเมื่อ :values มีอยู่',
'multiple_of' => 'ฟิลด์ :attribute ต้องเป็นพหุคูณของ :value',
'not_in' => ':attribute ที่เลือกไม่ถูกต้อง',
'not_regex' => 'รูปแบบฟิลด์ :attribute ไม่ถูกต้อง',
'numeric' => 'ฟิลด์ :attribute ต้องเป็นตัวเลข',
'password' => [
'letters' => 'ฟิลด์ :attribute ต้องมีอย่างน้อยหนึ่งตัวอักษร',
'mixed' => 'ฟิลด์ :attribute ต้องมีอย่างน้อยหนึ่งตัวพิมพ์ใหญ่และหนึ่งตัวพิมพ์เล็ก',
'numbers' => 'ฟิลด์ :attribute ต้องมีอย่างน้อยหนึ่งตัวเลข',
'symbols' => 'ฟิลด์ :attribute ต้องมีอย่างน้อยหนึ่งสัญลักษณ์',
'uncompromised' => ':attribute ที่ให้มามีการปรากฏในการรั่วไหลของข้อมูล กรุณาเลือก :attribute อื่น',
],
'present' => 'ฟิลด์ :attribute ต้องมีอยู่',
'present_if' => 'ฟิลด์ :attribute ต้องมีอยู่เมื่อ :other เป็น :value',
'present_unless' => 'ฟิลด์ :attribute ต้องมีอยู่เว้นแต่ :other เป็น :value',
'present_with' => 'ฟิลด์ :attribute ต้องมีอยู่เมื่อ :values มีอยู่',
'present_with_all' => 'ฟิลด์ :attribute ต้องมีอยู่เมื่อ :values มีอยู่',
'prohibited' => 'ฟิลด์ :attribute ถูกห้าม',
'prohibited_if' => 'ฟิลด์ :attribute ถูกห้ามเมื่อ :other เป็น :value',
'prohibited_if_accepted' => 'ฟิลด์ :attribute ถูกห้ามเมื่อ :other ถูกยอมรับ',
'prohibited_if_declined' => 'ฟิลด์ :attribute ถูกห้ามเมื่อ :other ถูกปฏิเสธ',
'prohibited_unless' => 'ฟิลด์ :attribute ถูกห้ามเว้นแต่ :other อยู่ใน :values',
'prohibits' => 'ฟิลด์ :attribute ห้าม :other มีอยู่',
'regex' => 'รูปแบบฟิลด์ :attribute ไม่ถูกต้อง',
'required' => 'ฟิลด์ :attribute จำเป็นต้องมี',
'required_array_keys' => 'ฟิลด์ :attribute ต้องมีรายการสำหรับ: :values',
'required_if' => 'ฟิลด์ :attribute จำเป็นต้องมีเมื่อ :other เป็น :value',
'required_if_accepted' => 'ฟิลด์ :attribute จำเป็นต้องมีเมื่อ :other ถูกยอมรับ',
'required_if_declined' => 'ฟิลด์ :attribute จำเป็นต้องมีเมื่อ :other ถูกปฏิเสธ',
'required_unless' => 'ฟิลด์ :attribute จำเป็นต้องมีเว้นแต่ :other อยู่ใน :values',
'required_with' => 'ฟิลด์ :attribute จำเป็นต้องมีเมื่อ :values มีอยู่',
'required_with_all' => 'ฟิลด์ :attribute จำเป็นต้องมีเมื่อ :values มีอยู่',
'required_without' => 'ฟิลด์ :attribute จำเป็นต้องมีเมื่อ :values ไม่มีอยู่',
'required_without_all' => 'ฟิลด์ :attribute จำเป็นต้องมีเมื่อไม่มี :values',
'same' => 'ฟิลด์ :attribute และ :other ต้องตรงกัน',
'size' => [
'array' => 'ฟิลด์ :attribute ต้องมี :size รายการ',
'file' => 'ฟิลด์ :attribute ต้องมีขนาด :size กิโลไบต์',
'numeric' => 'ฟิลด์ :attribute ต้องเป็น :size',
'string' => 'ฟิลด์ :attribute ต้องมีความยาว :size ตัวอักษร',
],
'starts_with' => 'ฟิลด์ :attribute ต้องขึ้นต้นด้วย: :values',
'string' => 'ฟิลด์ :attribute ต้องเป็นสตริง',
'timezone' => 'ฟิลด์ :attribute ต้องเป็นเขตเวลาที่ถูกต้อง',
'unique' => ':attribute ถูกใช้ไปแล้ว',
'uploaded' => 'การอัปโหลด :attribute ล้มเหลว',
'uppercase' => 'ฟิลด์ :attribute ต้องเป็นตัวพิมพ์ใหญ่',
'url' => 'ฟิลด์ :attribute ต้องเป็น URL ที่ถูกต้อง',
'ulid' => 'ฟิลด์ :attribute ต้องเป็น ULID ที่ถูกต้อง',
'uuid' => 'ฟิลด์ :attribute ต้องเป็น UUID ที่ถูกต้อง',
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
'attributes' => [],
];
@@ -0,0 +1,170 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Doğrulama mesajları
|--------------------------------------------------------------------------
*/
'accepted' => ':attribute alanı kabul edilmelidir.',
'accepted_if' => ':other :value olduğunda :attribute alanı kabul edilmelidir.',
'active_url' => ':attribute alanı geçerli bir URL olmalıdır.',
'after' => ':attribute alanı :date tarihinden sonra bir tarih olmalıdır.',
'after_or_equal' => ':attribute alanı :date tarihinden sonra veya aynı tarihte olmalıdır.',
'alpha' => ':attribute alanı yalnızca harf içermelidir.',
'alpha_dash' => ':attribute alanı yalnızca harf, rakam, tire ve alt çizgi içermelidir.',
'alpha_num' => ':attribute alanı yalnızca harf ve rakam içermelidir.',
'any_of' => ':attribute alanı geçersiz.',
'array' => ':attribute alanı bir dizi olmalıdır.',
'ascii' => ':attribute alanı yalnızca tek baytlık alfanümerik karakterler ve semboller içermelidir.',
'before' => ':attribute alanı :date tarihinden önce bir tarih olmalıdır.',
'before_or_equal' => ':attribute alanı :date tarihinden önce veya aynı tarihte olmalıdır.',
'between' => [
'array' => ':attribute alanı :min ile :max öğe arasında olmalıdır.',
'file' => ':attribute alanı :min ile :max kilobayt arasında olmalıdır.',
'numeric' => ':attribute alanı :min ile :max arasında olmalıdır.',
'string' => ':attribute alanı :min ile :max karakter arasında olmalıdır.',
],
'boolean' => ':attribute alanı doğru veya yanlış olmalıdır.',
'can' => ':attribute alanı yetkisiz bir değer içeriyor.',
'confirmed' => ':attribute alanı onayı eşleşmiyor.',
'contains' => ':attribute alanı gerekli bir değer içermiyor.',
'current_password' => 'Şifre yanlış.',
'date' => ':attribute alanı geçerli bir tarih olmalıdır.',
'date_equals' => ':attribute alanı :date tarihine eşit bir tarih olmalıdır.',
'date_format' => ':attribute alanı :format biçimine uymalıdır.',
'decimal' => ':attribute alanı :decimal ondalık basamağa sahip olmalıdır.',
'declined' => ':attribute alanı reddedilmelidir.',
'declined_if' => ':other :value olduğunda :attribute alanı reddedilmelidir.',
'different' => ':attribute ve :other alanları farklı olmalıdır.',
'digits' => ':attribute alanı :digits basamaklı olmalıdır.',
'digits_between' => ':attribute alanı :min ile :max basamak arasında olmalıdır.',
'dimensions' => ':attribute alanının görsel boyutları geçersiz.',
'distinct' => ':attribute alanında yinelenen bir değer var.',
'doesnt_contain' => ':attribute alanı şunları içermemelidir: :values.',
'doesnt_end_with' => ':attribute alanı şunlarla bitmemelidir: :values.',
'doesnt_start_with' => ':attribute alanı şunlarla başlamamalıdır: :values.',
'email' => ':attribute alanı geçerli bir e-posta adresi olmalıdır.',
'encoding' => ':attribute alanı :encoding ile kodlanmış olmalıdır.',
'ends_with' => ':attribute alanı şunlarla bitmelidir: :values.',
'enum' => 'Seçilen :attribute geçersiz.',
'exists' => 'Seçilen :attribute geçersiz.',
'extensions' => ':attribute alanı şu uzantılardan birine sahip olmalıdır: :values.',
'file' => ':attribute alanı bir dosya olmalıdır.',
'filled' => ':attribute alanının bir değeri olmalıdır.',
'gt' => [
'array' => ':attribute alanı :value öğeden fazla içermelidir.',
'file' => ':attribute alanı :value kilobayttan büyük olmalıdır.',
'numeric' => ':attribute alanı :value değerinden büyük olmalıdır.',
'string' => ':attribute alanı :value karakterden fazla içermelidir.',
],
'gte' => [
'array' => ':attribute alanı :value veya daha fazla öğe içermelidir.',
'file' => ':attribute alanı :value kilobayt veya daha fazla olmalıdır.',
'numeric' => ':attribute alanı :value veya daha fazla olmalıdır.',
'string' => ':attribute alanı :value veya daha fazla karakter içermelidir.',
],
'hex_color' => ':attribute alanı geçerli bir onaltılık renk olmalıdır.',
'image' => ':attribute alanı bir görsel olmalıdır.',
'in' => 'Seçilen :attribute geçersiz.',
'in_array' => ':attribute alanı :other içinde bulunmalıdır.',
'in_array_keys' => ':attribute alanı şu anahtarlardan en az birini içermelidir: :values.',
'integer' => ':attribute alanı bir tam sayı olmalıdır.',
'ip' => ':attribute alanı geçerli bir IP adresi olmalıdır.',
'ipv4' => ':attribute alanı geçerli bir IPv4 adresi olmalıdır.',
'ipv6' => ':attribute alanı geçerli bir IPv6 adresi olmalıdır.',
'json' => ':attribute alanı geçerli bir JSON dizisi olmalıdır.',
'list' => ':attribute alanı bir liste olmalıdır.',
'lowercase' => ':attribute alanı küçük harf olmalıdır.',
'lt' => [
'array' => ':attribute alanı :value öğeden az içermelidir.',
'file' => ':attribute alanı :value kilobayttan küçük olmalıdır.',
'numeric' => ':attribute alanı :value değerinden küçük olmalıdır.',
'string' => ':attribute alanı :value karakterden az içermelidir.',
],
'lte' => [
'array' => ':attribute alanı :value öğeden fazla içermemelidir.',
'file' => ':attribute alanı :value kilobayt veya daha az olmalıdır.',
'numeric' => ':attribute alanı :value veya daha az olmalıdır.',
'string' => ':attribute alanı :value karakter veya daha az içermelidir.',
],
'mac_address' => ':attribute alanı geçerli bir MAC adresi olmalıdır.',
'max' => [
'array' => ':attribute alanı :max öğeden fazla içermemelidir.',
'file' => ':attribute alanı :max kilobayttan büyük olmamalıdır.',
'numeric' => ':attribute alanı :max değerinden büyük olmamalıdır.',
'string' => ':attribute alanı :max karakterden fazla içermemelidir.',
],
'max_digits' => ':attribute alanı :max basamaktan fazla içermemelidir.',
'mimes' => ':attribute alanı şu türde bir dosya olmalıdır: :values.',
'mimetypes' => ':attribute alanı şu türde bir dosya olmalıdır: :values.',
'min' => [
'array' => ':attribute alanı en az :min öğe içermelidir.',
'file' => ':attribute alanı en az :min kilobayt olmalıdır.',
'numeric' => ':attribute alanı en az :min olmalıdır.',
'string' => ':attribute alanı en az :min karakter içermelidir.',
],
'min_digits' => ':attribute alanı en az :min basamak içermelidir.',
'missing' => ':attribute alanı eksik olmalıdır.',
'missing_if' => ':other :value olduğunda :attribute alanı eksik olmalıdır.',
'missing_unless' => ':other :value olmadıkça :attribute alanı eksik olmalıdır.',
'missing_with' => ':values mevcut olduğunda :attribute alanı eksik olmalıdır.',
'missing_with_all' => ':values mevcut olduğunda :attribute alanı eksik olmalıdır.',
'multiple_of' => ':attribute alanı :value\'nin katı olmalıdır.',
'not_in' => 'Seçilen :attribute geçersiz.',
'not_regex' => ':attribute alanı biçimi geçersiz.',
'numeric' => ':attribute alanı bir sayı olmalıdır.',
'password' => [
'letters' => ':attribute alanı en az bir harf içermelidir.',
'mixed' => ':attribute alanı en az bir büyük ve bir küçük harf içermelidir.',
'numbers' => ':attribute alanı en az bir rakam içermelidir.',
'symbols' => ':attribute alanı en az bir sembol içermelidir.',
'uncompromised' => 'Verilen :attribute bir veri sızıntısında göründü. Lütfen farklı bir :attribute seçin.',
],
'present' => ':attribute alanı mevcut olmalıdır.',
'present_if' => ':other :value olduğunda :attribute alanı mevcut olmalıdır.',
'present_unless' => ':other :value olmadıkça :attribute alanı mevcut olmalıdır.',
'present_with' => ':values mevcut olduğunda :attribute alanı mevcut olmalıdır.',
'present_with_all' => ':values mevcut olduğunda :attribute alanı mevcut olmalıdır.',
'prohibited' => ':attribute alanı yasaktır.',
'prohibited_if' => ':other :value olduğunda :attribute alanı yasaktır.',
'prohibited_if_accepted' => ':other kabul edildiğinde :attribute alanı yasaktır.',
'prohibited_if_declined' => ':other reddedildiğinde :attribute alanı yasaktır.',
'prohibited_unless' => ':other :values içinde olmadıkça :attribute alanı yasaktır.',
'prohibits' => ':attribute alanı :other\'ın mevcut olmasını yasaklar.',
'regex' => ':attribute alanı biçimi geçersiz.',
'required' => ':attribute alanı zorunludur.',
'required_array_keys' => ':attribute alanı şunlar için girişler içermelidir: :values.',
'required_if' => ':other :value olduğunda :attribute alanı zorunludur.',
'required_if_accepted' => ':other kabul edildiğinde :attribute alanı zorunludur.',
'required_if_declined' => ':other reddedildiğinde :attribute alanı zorunludur.',
'required_unless' => ':other :values içinde olmadıkça :attribute alanı zorunludur.',
'required_with' => ':values mevcut olduğunda :attribute alanı zorunludur.',
'required_with_all' => ':values mevcut olduğunda :attribute alanı zorunludur.',
'required_without' => ':values mevcut olmadığında :attribute alanı zorunludur.',
'required_without_all' => ':values\'tan hiçbiri mevcut olmadığında :attribute alanı zorunludur.',
'same' => ':attribute ve :other alanları eşleşmelidir.',
'size' => [
'array' => ':attribute alanı :size öğe içermelidir.',
'file' => ':attribute alanı :size kilobayt olmalıdır.',
'numeric' => ':attribute alanı :size olmalıdır.',
'string' => ':attribute alanı :size karakter içermelidir.',
],
'starts_with' => ':attribute alanı şunlarla başlamalıdır: :values.',
'string' => ':attribute alanı bir dizge olmalıdır.',
'timezone' => ':attribute alanı geçerli bir saat dilimi olmalıdır.',
'unique' => ':attribute zaten kullanılmış.',
'uploaded' => ':attribute yüklenemedi.',
'uppercase' => ':attribute alanı büyük harf olmalıdır.',
'url' => ':attribute alanı geçerli bir URL olmalıdır.',
'ulid' => ':attribute alanı geçerli bir ULID olmalıdır.',
'uuid' => ':attribute alanı geçerli bir UUID olmalıdır.',
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
'attributes' => [],
];
@@ -0,0 +1,170 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Thông báo xác thực
|--------------------------------------------------------------------------
*/
'accepted' => 'Trường :attribute phải được chấp nhận.',
'accepted_if' => 'Trường :attribute phải được chấp nhận khi :other là :value.',
'active_url' => 'Trường :attribute phải là URL hợp lệ.',
'after' => 'Trường :attribute phải là ngày sau :date.',
'after_or_equal' => 'Trường :attribute phải là ngày sau hoặc bằng :date.',
'alpha' => 'Trường :attribute chỉ được chứa chữ cái.',
'alpha_dash' => 'Trường :attribute chỉ được chứa chữ cái, số, dấu gạch ngang và gạch dưới.',
'alpha_num' => 'Trường :attribute chỉ được chứa chữ cái và số.',
'any_of' => 'Trường :attribute không hợp lệ.',
'array' => 'Trường :attribute phải là mảng.',
'ascii' => 'Trường :attribute chỉ được chứa ký tự chữ số và ký hiệu một byte.',
'before' => 'Trường :attribute phải là ngày trước :date.',
'before_or_equal' => 'Trường :attribute phải là ngày trước hoặc bằng :date.',
'between' => [
'array' => 'Trường :attribute phải có từ :min đến :max phần tử.',
'file' => 'Trường :attribute phải từ :min đến :max kilobyte.',
'numeric' => 'Trường :attribute phải từ :min đến :max.',
'string' => 'Trường :attribute phải có từ :min đến :max ký tự.',
],
'boolean' => 'Trường :attribute phải là đúng hoặc sai.',
'can' => 'Trường :attribute chứa giá trị không được phép.',
'confirmed' => 'Xác nhận trường :attribute không khớp.',
'contains' => 'Trường :attribute thiếu giá trị bắt buộc.',
'current_password' => 'Mật khẩu không đúng.',
'date' => 'Trường :attribute phải là ngày hợp lệ.',
'date_equals' => 'Trường :attribute phải là ngày bằng :date.',
'date_format' => 'Trường :attribute phải khớp với định dạng :format.',
'decimal' => 'Trường :attribute phải có :decimal chữ số thập phân.',
'declined' => 'Trường :attribute phải bị từ chối.',
'declined_if' => 'Trường :attribute phải bị từ chối khi :other là :value.',
'different' => 'Các trường :attribute và :other phải khác nhau.',
'digits' => 'Trường :attribute phải có :digits chữ số.',
'digits_between' => 'Trường :attribute phải có từ :min đến :max chữ số.',
'dimensions' => 'Trường :attribute có kích thước hình ảnh không hợp lệ.',
'distinct' => 'Trường :attribute có giá trị trùng lặp.',
'doesnt_contain' => 'Trường :attribute không được chứa: :values.',
'doesnt_end_with' => 'Trường :attribute không được kết thúc bằng: :values.',
'doesnt_start_with' => 'Trường :attribute không được bắt đầu bằng: :values.',
'email' => 'Trường :attribute phải là địa chỉ email hợp lệ.',
'encoding' => 'Trường :attribute phải được mã hóa bằng :encoding.',
'ends_with' => 'Trường :attribute phải kết thúc bằng: :values.',
'enum' => ':attribute được chọn không hợp lệ.',
'exists' => ':attribute được chọn không hợp lệ.',
'extensions' => 'Trường :attribute phải có một trong các phần mở rộng sau: :values.',
'file' => 'Trường :attribute phải là tệp.',
'filled' => 'Trường :attribute phải có giá trị.',
'gt' => [
'array' => 'Trường :attribute phải có nhiều hơn :value phần tử.',
'file' => 'Trường :attribute phải lớn hơn :value kilobyte.',
'numeric' => 'Trường :attribute phải lớn hơn :value.',
'string' => 'Trường :attribute phải có nhiều hơn :value ký tự.',
],
'gte' => [
'array' => 'Trường :attribute phải có :value phần tử trở lên.',
'file' => 'Trường :attribute phải lớn hơn hoặc bằng :value kilobyte.',
'numeric' => 'Trường :attribute phải lớn hơn hoặc bằng :value.',
'string' => 'Trường :attribute phải có :value ký tự trở lên.',
],
'hex_color' => 'Trường :attribute phải là màu hex hợp lệ.',
'image' => 'Trường :attribute phải là hình ảnh.',
'in' => ':attribute được chọn không hợp lệ.',
'in_array' => 'Trường :attribute phải tồn tại trong :other.',
'in_array_keys' => 'Trường :attribute phải chứa ít nhất một trong các khóa sau: :values.',
'integer' => 'Trường :attribute phải là số nguyên.',
'ip' => 'Trường :attribute phải là địa chỉ IP hợp lệ.',
'ipv4' => 'Trường :attribute phải là địa chỉ IPv4 hợp lệ.',
'ipv6' => 'Trường :attribute phải là địa chỉ IPv6 hợp lệ.',
'json' => 'Trường :attribute phải là chuỗi JSON hợp lệ.',
'list' => 'Trường :attribute phải là danh sách.',
'lowercase' => 'Trường :attribute phải viết thường.',
'lt' => [
'array' => 'Trường :attribute phải có ít hơn :value phần tử.',
'file' => 'Trường :attribute phải nhỏ hơn :value kilobyte.',
'numeric' => 'Trường :attribute phải nhỏ hơn :value.',
'string' => 'Trường :attribute phải có ít hơn :value ký tự.',
],
'lte' => [
'array' => 'Trường :attribute không được có quá :value phần tử.',
'file' => 'Trường :attribute phải nhỏ hơn hoặc bằng :value kilobyte.',
'numeric' => 'Trường :attribute phải nhỏ hơn hoặc bằng :value.',
'string' => 'Trường :attribute phải có :value ký tự trở xuống.',
],
'mac_address' => 'Trường :attribute phải là địa chỉ MAC hợp lệ.',
'max' => [
'array' => 'Trường :attribute không được có quá :max phần tử.',
'file' => 'Trường :attribute không được lớn hơn :max kilobyte.',
'numeric' => 'Trường :attribute không được lớn hơn :max.',
'string' => 'Trường :attribute không được có quá :max ký tự.',
],
'max_digits' => 'Trường :attribute không được có quá :max chữ số.',
'mimes' => 'Trường :attribute phải là tệp loại: :values.',
'mimetypes' => 'Trường :attribute phải là tệp loại: :values.',
'min' => [
'array' => 'Trường :attribute phải có ít nhất :min phần tử.',
'file' => 'Trường :attribute phải ít nhất :min kilobyte.',
'numeric' => 'Trường :attribute phải ít nhất :min.',
'string' => 'Trường :attribute phải có ít nhất :min ký tự.',
],
'min_digits' => 'Trường :attribute phải có ít nhất :min chữ số.',
'missing' => 'Trường :attribute phải vắng mặt.',
'missing_if' => 'Trường :attribute phải vắng mặt khi :other là :value.',
'missing_unless' => 'Trường :attribute phải vắng mặt trừ khi :other là :value.',
'missing_with' => 'Trường :attribute phải vắng mặt khi :values có mặt.',
'missing_with_all' => 'Trường :attribute phải vắng mặt khi :values có mặt.',
'multiple_of' => 'Trường :attribute phải là bội số của :value.',
'not_in' => ':attribute được chọn không hợp lệ.',
'not_regex' => 'Định dạng trường :attribute không hợp lệ.',
'numeric' => 'Trường :attribute phải là số.',
'password' => [
'letters' => 'Trường :attribute phải chứa ít nhất một chữ cái.',
'mixed' => 'Trường :attribute phải chứa ít nhất một chữ hoa và một chữ thường.',
'numbers' => 'Trường :attribute phải chứa ít nhất một số.',
'symbols' => 'Trường :attribute phải chứa ít nhất một ký hiệu.',
'uncompromised' => ':attribute đã xuất hiện trong rò rỉ dữ liệu. Vui lòng chọn :attribute khác.',
],
'present' => 'Trường :attribute phải có mặt.',
'present_if' => 'Trường :attribute phải có mặt khi :other là :value.',
'present_unless' => 'Trường :attribute phải có mặt trừ khi :other là :value.',
'present_with' => 'Trường :attribute phải có mặt khi :values có mặt.',
'present_with_all' => 'Trường :attribute phải có mặt khi :values có mặt.',
'prohibited' => 'Trường :attribute bị cấm.',
'prohibited_if' => 'Trường :attribute bị cấm khi :other là :value.',
'prohibited_if_accepted' => 'Trường :attribute bị cấm khi :other được chấp nhận.',
'prohibited_if_declined' => 'Trường :attribute bị cấm khi :other bị từ chối.',
'prohibited_unless' => 'Trường :attribute bị cấm trừ khi :other nằm trong :values.',
'prohibits' => 'Trường :attribute cấm :other có mặt.',
'regex' => 'Định dạng trường :attribute không hợp lệ.',
'required' => 'Trường :attribute là bắt buộc.',
'required_array_keys' => 'Trường :attribute phải chứa mục cho: :values.',
'required_if' => 'Trường :attribute là bắt buộc khi :other là :value.',
'required_if_accepted' => 'Trường :attribute là bắt buộc khi :other được chấp nhận.',
'required_if_declined' => 'Trường :attribute là bắt buộc khi :other bị từ chối.',
'required_unless' => 'Trường :attribute là bắt buộc trừ khi :other nằm trong :values.',
'required_with' => 'Trường :attribute là bắt buộc khi :values có mặt.',
'required_with_all' => 'Trường :attribute là bắt buộc khi :values có mặt.',
'required_without' => 'Trường :attribute là bắt buộc khi :values vắng mặt.',
'required_without_all' => 'Trường :attribute là bắt buộc khi không có :values nào có mặt.',
'same' => 'Các trường :attribute và :other phải khớp.',
'size' => [
'array' => 'Trường :attribute phải chứa :size phần tử.',
'file' => 'Trường :attribute phải là :size kilobyte.',
'numeric' => 'Trường :attribute phải là :size.',
'string' => 'Trường :attribute phải có :size ký tự.',
],
'starts_with' => 'Trường :attribute phải bắt đầu bằng: :values.',
'string' => 'Trường :attribute phải là chuỗi.',
'timezone' => 'Trường :attribute phải là múi giờ hợp lệ.',
'unique' => ':attribute đã được sử dụng.',
'uploaded' => 'Tải lên :attribute thất bại.',
'uppercase' => 'Trường :attribute phải viết hoa.',
'url' => 'Trường :attribute phải là URL hợp lệ.',
'ulid' => 'Trường :attribute phải là ULID hợp lệ.',
'uuid' => 'Trường :attribute phải là UUID hợp lệ.',
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
'attributes' => [],
];
@@ -0,0 +1,180 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| 验证提示语
|--------------------------------------------------------------------------
*/
'accepted' => ':attribute 必须接受。',
'accepted_if' => '当 :other 为 :value 时 :attribute 必须接受。',
'active_url' => ':attribute 必须是一个有效的 URL。',
'after' => ':attribute 必须是 :date 之后的日期。',
'after_or_equal' => ':attribute 必须是 :date 之后或相同的日期。',
'alpha' => ':attribute 只能包含字母。',
'alpha_dash' => ':attribute 只能包含字母、数字、短横线和下划线。',
'alpha_num' => ':attribute 只能包含字母和数字。',
'any_of' => ':attribute 无效。',
'array' => ':attribute 必须是数组。',
'ascii' => ':attribute 只能包含 ASCII 字符。',
'before' => ':attribute 必须是 :date 之前的日期。',
'before_or_equal' => ':attribute 必须是 :date 之前或相同的日期。',
'between' => [
'array' => ':attribute 必须在 :min 到 :max 个之间。',
'file' => ':attribute 必须在 :min 到 :max KB 之间。',
'numeric' => ':attribute 必须在 :min 到 :max 之间。',
'string' => ':attribute 必须在 :min 到 :max 个字符之间。',
],
'boolean' => ':attribute 必须是布尔值。',
'can' => ':attribute 包含未授权的值。',
'confirmed' => ':attribute 两次输入不一致。',
'contains' => ':attribute 缺少必需值。',
'current_password' => '密码不正确。',
'date' => ':attribute 必须是有效日期。',
'date_equals' => ':attribute 必须等于 :date。',
'date_format' => ':attribute 格式必须为 :format。',
'decimal' => ':attribute 必须有 :decimal 位小数。',
'declined' => ':attribute 必须拒绝。',
'declined_if' => '当 :other 为 :value 时 :attribute 必须拒绝。',
'different' => ':attribute 与 :other 必须不同。',
'digits' => ':attribute 必须是 :digits 位数字。',
'digits_between' => ':attribute 必须在 :min 到 :max 位数字之间。',
'dimensions' => ':attribute 图片尺寸无效。',
'distinct' => ':attribute 存在重复值。',
'doesnt_contain' => ':attribute 不能包含以下内容::values。',
'doesnt_end_with' => ':attribute 不能以下列之一结尾::values。',
'doesnt_start_with' => ':attribute 不能以下列之一开头::values。',
'email' => ':attribute 必须是有效的邮箱地址。',
'encoding' => ':attribute 必须使用 :encoding 编码。',
'ends_with' => ':attribute 必须以下列之一结尾::values。',
'enum' => '所选的 :attribute 无效。',
'exists' => '所选的 :attribute 无效。',
'extensions' => ':attribute 必须具有以下扩展名之一::values。',
'file' => ':attribute 必须是文件。',
'filled' => ':attribute 必须有值。',
'gt' => [
'array' => ':attribute 必须多于 :value 个元素。',
'file' => ':attribute 必须大于 :value KB。',
'numeric' => ':attribute 必须大于 :value。',
'string' => ':attribute 必须大于 :value 个字符。',
],
'gte' => [
'array' => ':attribute 必须不少于 :value 个元素。',
'file' => ':attribute 必须大于等于 :value KB。',
'numeric' => ':attribute 必须大于等于 :value。',
'string' => ':attribute 必须大于等于 :value 个字符。',
],
'hex_color' => ':attribute 必须是有效的十六进制颜色值。',
'image' => ':attribute 必须是图片。',
'in' => '所选的 :attribute 无效。',
'in_array' => ':attribute 必须存在于 :other 中。',
'in_array_keys' => ':attribute 必须包含以下键之一::values。',
'integer' => ':attribute 必须是整数。',
'ip' => ':attribute 必须是有效的 IP 地址。',
'ipv4' => ':attribute 必须是有效的 IPv4 地址。',
'ipv6' => ':attribute 必须是有效的 IPv6 地址。',
'json' => ':attribute 必须是有效的 JSON 字符串。',
'list' => ':attribute 必须是列表。',
'lowercase' => ':attribute 必须是小写。',
'lt' => [
'array' => ':attribute 必须少于 :value 个元素。',
'file' => ':attribute 必须小于 :value KB。',
'numeric' => ':attribute 必须小于 :value。',
'string' => ':attribute 必须小于 :value 个字符。',
],
'lte' => [
'array' => ':attribute 不能多于 :value 个元素。',
'file' => ':attribute 必须小于等于 :value KB。',
'numeric' => ':attribute 必须小于等于 :value。',
'string' => ':attribute 必须小于等于 :value 个字符。',
],
'mac_address' => ':attribute 必须是有效的 MAC 地址。',
'max' => [
'array' => ':attribute 不能超过 :max 个元素。',
'file' => ':attribute 不能大于 :max KB。',
'numeric' => ':attribute 不能大于 :max。',
'string' => ':attribute 不能大于 :max 个字符。',
],
'max_digits' => ':attribute 不能超过 :max 位数字。',
'mimes' => ':attribute 必须是 :values 类型的文件。',
'mimetypes' => ':attribute 必须是 :values 类型的文件。',
'min' => [
'array' => ':attribute 至少有 :min 个元素。',
'file' => ':attribute 至少 :min KB。',
'numeric' => ':attribute 至少 :min。',
'string' => ':attribute 至少 :min 个字符。',
],
'min_digits' => ':attribute 至少 :min 位数字。',
'missing' => ':attribute 必须不存在。',
'missing_if' => '当 :other 为 :value 时 :attribute 必须不存在。',
'missing_unless' => '除非 :other 为 :value,否则 :attribute 必须不存在。',
'missing_with' => '当 :values 存在时 :attribute 必须不存在。',
'missing_with_all' => '当 :values 都存在时 :attribute 必须不存在。',
'multiple_of' => ':attribute 必须是 :value 的倍数。',
'not_in' => '所选的 :attribute 无效。',
'not_regex' => ':attribute 格式无效。',
'numeric' => ':attribute 必须是数字。',
'password' => [
'letters' => ':attribute 必须包含至少一个字母。',
'mixed' => ':attribute 必须包含至少一个大写和一个小写字母。',
'numbers' => ':attribute 必须包含至少一个数字。',
'symbols' => ':attribute 必须包含至少一个符号。',
'uncompromised' => ':attribute 已出现在数据泄露中,请更换。',
],
'present' => ':attribute 必须存在。',
'present_if' => '当 :other 为 :value 时 :attribute 必须存在。',
'present_unless' => '除非 :other 为 :value,否则 :attribute 必须存在。',
'present_with' => '当 :values 存在时 :attribute 必须存在。',
'present_with_all' => '当 :values 都存在时 :attribute 必须存在。',
'prohibited' => ':attribute 被禁止。',
'prohibited_if' => '当 :other 为 :value 时 :attribute 被禁止。',
'prohibited_if_accepted' => '当 :other 被接受时 :attribute 被禁止。',
'prohibited_if_declined' => '当 :other 被拒绝时 :attribute 被禁止。',
'prohibited_unless' => '除非 :other 在 :values 中,否则 :attribute 被禁止。',
'prohibits' => ':attribute 禁止 :other 出现。',
'regex' => ':attribute 格式无效。',
'required' => ':attribute 为必填项。',
'required_array_keys' => ':attribute 必须包含以下键::values。',
'required_if' => '当 :other 为 :value 时 :attribute 为必填项。',
'required_if_accepted' => '当 :other 被接受时 :attribute 为必填项。',
'required_if_declined' => '当 :other 被拒绝时 :attribute 为必填项。',
'required_unless' => '除非 :other 在 :values 中,否则 :attribute 为必填项。',
'required_with' => '当 :values 存在时 :attribute 为必填项。',
'required_with_all' => '当 :values 都存在时 :attribute 为必填项。',
'required_without' => '当 :values 不存在时 :attribute 为必填项。',
'required_without_all' => '当 :values 都不存在时 :attribute 为必填项。',
'same' => ':attribute 与 :other 必须相同。',
'size' => [
'array' => ':attribute 必须包含 :size 个元素。',
'file' => ':attribute 必须为 :size KB。',
'numeric' => ':attribute 必须为 :size。',
'string' => ':attribute 必须为 :size 个字符。',
],
'starts_with' => ':attribute 必须以下列之一开头::values。',
'string' => ':attribute 必须是字符串。',
'timezone' => ':attribute 必须是有效的时区。',
'unique' => ':attribute 已存在。',
'uploaded' => ':attribute 上传失败。',
'uppercase' => ':attribute 必须是大写。',
'url' => ':attribute 必须是有效的 URL。',
'ulid' => ':attribute 必须是有效的 ULID。',
'uuid' => ':attribute 必须是有效的 UUID。',
/*
|--------------------------------------------------------------------------
| 自定义验证语言行
|--------------------------------------------------------------------------
*/
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
/*
|--------------------------------------------------------------------------
| 自定义属性名称
|--------------------------------------------------------------------------
*/
'attributes' => [],
];
@@ -0,0 +1,170 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| 驗證提示語
|--------------------------------------------------------------------------
*/
'accepted' => ':attribute 必須接受。',
'accepted_if' => '當 :other 為 :value 時 :attribute 必須接受。',
'active_url' => ':attribute 必須是一個有效的 URL。',
'after' => ':attribute 必須是 :date 之後的日期。',
'after_or_equal' => ':attribute 必須是 :date 之後或相同的日期。',
'alpha' => ':attribute 只能包含字母。',
'alpha_dash' => ':attribute 只能包含字母、數字、短橫線和底線。',
'alpha_num' => ':attribute 只能包含字母和數字。',
'any_of' => ':attribute 無效。',
'array' => ':attribute 必須是陣列。',
'ascii' => ':attribute 只能包含 ASCII 字元。',
'before' => ':attribute 必須是 :date 之前的日期。',
'before_or_equal' => ':attribute 必須是 :date 之前或相同的日期。',
'between' => [
'array' => ':attribute 必須在 :min 到 :max 個之間。',
'file' => ':attribute 必須在 :min 到 :max KB 之間。',
'numeric' => ':attribute 必須在 :min 到 :max 之間。',
'string' => ':attribute 必須在 :min 到 :max 個字元之間。',
],
'boolean' => ':attribute 必須是布林值。',
'can' => ':attribute 包含未授權的值。',
'confirmed' => ':attribute 兩次輸入不一致。',
'contains' => ':attribute 缺少必需值。',
'current_password' => '密碼不正確。',
'date' => ':attribute 必須是有效日期。',
'date_equals' => ':attribute 必須等於 :date。',
'date_format' => ':attribute 格式必須為 :format。',
'decimal' => ':attribute 必須有 :decimal 位小數。',
'declined' => ':attribute 必須拒絕。',
'declined_if' => '當 :other 為 :value 時 :attribute 必須拒絕。',
'different' => ':attribute 與 :other 必須不同。',
'digits' => ':attribute 必須是 :digits 位數字。',
'digits_between' => ':attribute 必須在 :min 到 :max 位數字之間。',
'dimensions' => ':attribute 圖片尺寸無效。',
'distinct' => ':attribute 存在重複值。',
'doesnt_contain' => ':attribute 不能包含以下內容::values。',
'doesnt_end_with' => ':attribute 不能以下列之一結尾::values。',
'doesnt_start_with' => ':attribute 不能以下列之一開頭::values。',
'email' => ':attribute 必須是有效的電子郵件地址。',
'encoding' => ':attribute 必須使用 :encoding 編碼。',
'ends_with' => ':attribute 必須以下列之一結尾::values。',
'enum' => '所選的 :attribute 無效。',
'exists' => '所選的 :attribute 無效。',
'extensions' => ':attribute 必須具有以下副檔名之一::values。',
'file' => ':attribute 必須是檔案。',
'filled' => ':attribute 必須有值。',
'gt' => [
'array' => ':attribute 必須多於 :value 個元素。',
'file' => ':attribute 必須大於 :value KB。',
'numeric' => ':attribute 必須大於 :value。',
'string' => ':attribute 必須大於 :value 個字元。',
],
'gte' => [
'array' => ':attribute 必須不少於 :value 個元素。',
'file' => ':attribute 必須大於等於 :value KB。',
'numeric' => ':attribute 必須大於等於 :value。',
'string' => ':attribute 必須大於等於 :value 個字元。',
],
'hex_color' => ':attribute 必須是有效的十六進位顏色值。',
'image' => ':attribute 必須是圖片。',
'in' => '所選的 :attribute 無效。',
'in_array' => ':attribute 必須存在於 :other 中。',
'in_array_keys' => ':attribute 必須包含以下鍵之一::values。',
'integer' => ':attribute 必須是整數。',
'ip' => ':attribute 必須是有效的 IP 位址。',
'ipv4' => ':attribute 必須是有效的 IPv4 位址。',
'ipv6' => ':attribute 必須是有效的 IPv6 位址。',
'json' => ':attribute 必須是有效的 JSON 字串。',
'list' => ':attribute 必須是列表。',
'lowercase' => ':attribute 必須是小寫。',
'lt' => [
'array' => ':attribute 必須少於 :value 個元素。',
'file' => ':attribute 必須小於 :value KB。',
'numeric' => ':attribute 必須小於 :value。',
'string' => ':attribute 必須小於 :value 個字元。',
],
'lte' => [
'array' => ':attribute 不能多於 :value 個元素。',
'file' => ':attribute 必須小於等於 :value KB。',
'numeric' => ':attribute 必須小於等於 :value。',
'string' => ':attribute 必須小於等於 :value 個字元。',
],
'mac_address' => ':attribute 必須是有效的 MAC 位址。',
'max' => [
'array' => ':attribute 不能超過 :max 個元素。',
'file' => ':attribute 不能大於 :max KB。',
'numeric' => ':attribute 不能大於 :max。',
'string' => ':attribute 不能大於 :max 個字元。',
],
'max_digits' => ':attribute 不能超過 :max 位數字。',
'mimes' => ':attribute 必須是 :values 類型的檔案。',
'mimetypes' => ':attribute 必須是 :values 類型的檔案。',
'min' => [
'array' => ':attribute 至少有 :min 個元素。',
'file' => ':attribute 至少 :min KB。',
'numeric' => ':attribute 至少 :min。',
'string' => ':attribute 至少 :min 個字元。',
],
'min_digits' => ':attribute 至少 :min 位數字。',
'missing' => ':attribute 必須不存在。',
'missing_if' => '當 :other 為 :value 時 :attribute 必須不存在。',
'missing_unless' => '除非 :other 為 :value,否則 :attribute 必須不存在。',
'missing_with' => '當 :values 存在時 :attribute 必須不存在。',
'missing_with_all' => '當 :values 都存在時 :attribute 必須不存在。',
'multiple_of' => ':attribute 必須是 :value 的倍數。',
'not_in' => '所選的 :attribute 無效。',
'not_regex' => ':attribute 格式無效。',
'numeric' => ':attribute 必須是數字。',
'password' => [
'letters' => ':attribute 必須包含至少一個字母。',
'mixed' => ':attribute 必須包含至少一個大寫和一個小寫字母。',
'numbers' => ':attribute 必須包含至少一個數字。',
'symbols' => ':attribute 必須包含至少一個符號。',
'uncompromised' => ':attribute 已出現在資料外洩中,請更換。',
],
'present' => ':attribute 必須存在。',
'present_if' => '當 :other 為 :value 時 :attribute 必須存在。',
'present_unless' => '除非 :other 為 :value,否則 :attribute 必須存在。',
'present_with' => '當 :values 存在時 :attribute 必須存在。',
'present_with_all' => '當 :values 都存在時 :attribute 必須存在。',
'prohibited' => ':attribute 被禁止。',
'prohibited_if' => '當 :other 為 :value 時 :attribute 被禁止。',
'prohibited_if_accepted' => '當 :other 被接受時 :attribute 被禁止。',
'prohibited_if_declined' => '當 :other 被拒絕時 :attribute 被禁止。',
'prohibited_unless' => '除非 :other 在 :values 中,否則 :attribute 被禁止。',
'prohibits' => ':attribute 禁止 :other 出現。',
'regex' => ':attribute 格式無效。',
'required' => ':attribute 為必填項。',
'required_array_keys' => ':attribute 必須包含以下鍵::values。',
'required_if' => '當 :other 為 :value 時 :attribute 為必填項。',
'required_if_accepted' => '當 :other 被接受時 :attribute 為必填項。',
'required_if_declined' => '當 :other 被拒絕時 :attribute 為必填項。',
'required_unless' => '除非 :other 在 :values 中,否則 :attribute 為必填項。',
'required_with' => '當 :values 存在時 :attribute 為必填項。',
'required_with_all' => '當 :values 都存在時 :attribute 為必填項。',
'required_without' => '當 :values 不存在時 :attribute 為必填項。',
'required_without_all' => '當 :values 都不存在時 :attribute 為必填項。',
'same' => ':attribute 與 :other 必須相同。',
'size' => [
'array' => ':attribute 必須包含 :size 個元素。',
'file' => ':attribute 必須為 :size KB。',
'numeric' => ':attribute 必須為 :size。',
'string' => ':attribute 必須為 :size 個字元。',
],
'starts_with' => ':attribute 必須以下列之一開頭::values。',
'string' => ':attribute 必須是字串。',
'timezone' => ':attribute 必須是有效的時區。',
'unique' => ':attribute 已存在。',
'uploaded' => ':attribute 上傳失敗。',
'uppercase' => ':attribute 必須是大寫。',
'url' => ':attribute 必須是有效的 URL。',
'ulid' => ':attribute 必須是有效的 ULID。',
'uuid' => ':attribute 必須是有效的 UUID。',
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
'attributes' => [],
];
+18
View File
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Webman\Validation\Annotation;
use Attribute;
#[Attribute(Attribute::TARGET_PARAMETER)]
class Param
{
public function __construct(
public string|array $rules = '',
public array $messages = [],
public string $attribute = '',
public string|array|null $in = null
) {
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Webman\Validation\Annotation;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Validate
{
public function __construct(
public array $rules = [],
public array $messages = [],
public array $attributes = [],
public ?string $validator = null,
public ?string $scene = null,
public string|array|null $in = null
) {
}
}
@@ -0,0 +1,710 @@
<?php
declare(strict_types=1);
namespace Webman\Validation\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Webman\Validation\Command\ValidatorGenerator\Illuminate\IlluminateConnectionResolver;
use Webman\Validation\Command\ValidatorGenerator\Rules\DefaultRuleInferrer;
use Webman\Validation\Command\ValidatorGenerator\Support\ExcludedColumns;
use Webman\Validation\Command\ValidatorGenerator\Support\OrmDetector;
use Webman\Validation\Command\ValidatorGenerator\Support\SchemaIntrospectorFactory;
use Webman\Validation\Command\ValidatorGenerator\ThinkOrm\ThinkOrmConnectionResolver;
use Webman\Validation\Command\ValidatorGenerator\Support\ValidatorClassRenderer;
use Webman\Validation\Command\ValidatorGenerator\Support\ValidatorFileWriter;
use Webman\Validation\Command\Messages;
#[AsCommand('make:validator', 'Make validation validator class.')]
final class MakeValidatorCommand extends Command
{
protected function configure(): void
{
$this->setDescription($this->selectByLocale(Messages::getDescription()));
$this->addArgument(
'name',
InputArgument::REQUIRED,
$this->selectByLocale(Messages::getArgumentName())
);
$this->addOption(
'plugin',
'p',
InputOption::VALUE_REQUIRED,
$this->selectByLocale(Messages::getOptionPlugin())
);
$this->addOption(
'path',
'P',
InputOption::VALUE_REQUIRED,
$this->selectByLocale(Messages::getOptionPath())
);
$this->addOption(
'force',
'f',
InputOption::VALUE_NONE,
$this->selectByLocale(Messages::getOptionForce())
);
$this->addOption(
'table',
't',
InputOption::VALUE_REQUIRED,
$this->selectByLocale(Messages::getOptionTable())
);
$this->addOption(
'database',
'd',
InputOption::VALUE_REQUIRED,
$this->selectByLocale(Messages::getOptionDatabase())
);
$this->addOption(
'scenes',
's',
InputOption::VALUE_REQUIRED,
$this->selectByLocale(Messages::getOptionScenes())
);
$this->addOption(
'orm',
'o',
InputOption::VALUE_REQUIRED,
$this->selectByLocale(Messages::getOptionOrm())
);
$this->setHelp($this->buildHelpText());
$this->addUsage('UserValidator');
$this->addUsage('admin/UserValidator');
$this->addUsage('UserValidator -p admin');
$this->addUsage('UserValidator -P plugin/admin/app/validation');
$this->addUsage('UserValidator -t users -d default');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$rawName = (string)$input->getArgument('name');
$name = $this->nameToClass($rawName);
$name = str_replace('\\', '/', $name);
$name = trim($name, '/');
$plugin = $this->normalizeOptionValue($input->getOption('plugin'));
$path = $this->normalizeOptionValue($input->getOption('path'));
$force = (bool)$input->getOption('force');
$table = $input->getOption('table');
$table = is_string($table) ? trim($table) : '';
// Some Symfony Console versions parse `-t=foo` as `=foo` for short options.
$table = ltrim($table, '=');
$databaseOptionRaw = $input->getOption('database');
$databaseOptionRaw = is_string($databaseOptionRaw) ? trim($databaseOptionRaw) : '';
// Some Symfony Console versions parse `-d=foo` as `=foo` for short options.
$databaseOptionRaw = ltrim($databaseOptionRaw, '=');
$connectionName = $databaseOptionRaw !== '' ? $databaseOptionRaw : null;
$scenesOption = $input->getOption('scenes');
$scenesOption = is_string($scenesOption) ? trim($scenesOption) : '';
// Some Symfony Console versions parse `-s=crud` as `=crud` for short options.
$scenesOption = ltrim($scenesOption, '=');
$ormOption = $input->getOption('orm');
$ormOption = is_string($ormOption) ? trim($ormOption) : OrmDetector::ORM_AUTO;
// Some Symfony Console versions parse `-o=xxx` as `=xxx` for short options.
$ormOption = ltrim($ormOption, '=');
if ($ormOption === '') {
$ormOption = OrmDetector::ORM_AUTO;
}
if ($name === '') {
$output->writeln($this->msg('invalid_name_empty'));
return self::FAILURE;
}
if ($plugin && (str_contains($plugin, '/') || str_contains($plugin, '\\'))) {
$output->writeln($this->msg('invalid_plugin', ['{plugin}' => $plugin]));
return self::FAILURE;
}
if ($plugin && !$this->pluginExists($plugin)) {
$output->writeln($this->msg('plugin_not_found', ['{plugin}' => $plugin]));
return self::FAILURE;
}
try {
if ($plugin || $path) {
$resolved = $this->resolveTargetByPluginOrPath(
$name,
$plugin,
$path,
$output,
fn(string $p): string => $this->getPluginValidationRelativePath($p),
fn(string $key, array $replace = []): string => $this->msg($key, $replace)
);
if ($resolved === null) {
return self::FAILURE;
}
[$class, $namespace, $file] = $resolved;
} else {
[$namespace, $class, $file] = $this->resolveAppValidationTarget($name);
}
} catch (\Throwable $e) {
$output->writeln($this->msg('invalid_name', ['{name}' => $rawName]));
$output->writeln($this->msg('reason', ['{reason}' => $e->getMessage()]));
return self::FAILURE;
}
if (is_file($file)) {
// Ask for confirmation when overwriting in interactive mode.
// If the environment is non-interactive, do not block on prompting.
if ($input->isInteractive()) {
$helper = $this->getHelper('question');
$relative = $this->toRelativePath($file);
$prompt = $this->msg('override_prompt', ['{path}' => $relative]);
$question = new ConfirmationQuestion($prompt, true);
if (!$helper->ask($input, $output, $question)) {
return self::SUCCESS;
}
} elseif (!$force) {
// Non-interactive mode and no --force: refuse to overwrite.
$output->writeln($this->msg('file_exists', ['{path}' => $this->toRelativePath($file)]));
$output->writeln($this->msg('use_force'));
return self::FAILURE;
}
}
$rules = [];
$messages = [];
$attributes = [];
$scenes = [];
if ($scenesOption !== '' && $table === '') {
$output->writeln($this->msg('scenes_requires_table'));
return self::FAILURE;
}
if ($table !== '') {
try {
$detector = new OrmDetector();
$orm = $detector->resolve($ormOption);
if (!in_array($orm, [OrmDetector::ORM_LARAVEL, OrmDetector::ORM_THINKORM], true)) {
$output->writeln($this->msg('unsupported_orm', ['{orm}' => (string)$orm]));
return self::FAILURE;
}
$resolver = $orm === OrmDetector::ORM_THINKORM
? new ThinkOrmConnectionResolver()
: new IlluminateConnectionResolver();
$resolvedConnectionName = $this->resolveDatabaseConnectionNameForTable(
$plugin,
$connectionName,
$databaseOptionRaw,
$orm,
$output
);
if ($resolvedConnectionName === null) {
return self::FAILURE;
}
$connection = $resolver->resolve($resolvedConnectionName);
$factory = new SchemaIntrospectorFactory();
$introspector = $factory->createForDriver($connection->driverName());
$tableDef = $introspector->introspect($connection, $table);
$excludeColumns = match ($orm) {
OrmDetector::ORM_THINKORM => ExcludedColumns::defaultForThinkOrm(),
default => ExcludedColumns::defaultForIlluminate(),
};
$inferrer = new DefaultRuleInferrer();
$result = $inferrer->infer($tableDef, [
'exclude_columns' => $excludeColumns,
'with_scenes' => $scenesOption !== '',
'scenes' => $scenesOption,
]);
$rules = $result['rules'] ?? [];
$attributes = $result['attributes'] ?? [];
$scenes = $result['scenes'] ?? [];
if ($rules === []) {
$output->writeln($this->msg('no_rules_from_table', ['{table}' => $table]));
return self::FAILURE;
}
} catch (\Throwable $e) {
$output->writeln($this->msg('failed_generate_from_table', ['{table}' => $table]));
$output->writeln($this->msg('reason', ['{reason}' => $e->getMessage()]));
return self::FAILURE;
}
}
$renderer = new ValidatorClassRenderer();
$content = $renderer->render($namespace, $class, $rules, $messages, $attributes, $scenes);
try {
(new ValidatorFileWriter())->write($file, $content);
} catch (\Throwable $e) {
$output->writeln($this->msg('failed_write_file', ['{path}' => $this->toRelativePath($file)]));
$output->writeln($this->msg('reason', ['{reason}' => $e->getMessage()]));
return self::FAILURE;
}
$output->writeln($this->msg('created', ['{path}' => $this->toRelativePath($file)]));
$output->writeln($this->msg('class', ['{class}' => $namespace . '\\' . $class]));
if ($table !== '') {
$output->writeln($this->msg('table', ['{table}' => $table]));
$output->writeln($this->msg('rules_count', ['{count}' => (string)count($rules)]));
if ($scenesOption !== '') {
$output->writeln($this->msg('scenes_count', ['{count}' => (string)count($scenes)]));
}
}
return self::SUCCESS;
}
/**
* @return array{0:string,1:string,2:string} [namespace, class, file]
*/
private function resolveAppValidationTarget(string $name): array
{
$name = trim($name);
if ($name === '') {
throw new \InvalidArgumentException($this->plain('invalid_name_empty_plain'));
}
// Normalize separators for Windows/Unix inputs.
$normalized = str_replace('\\', '/', $name);
$normalized = trim($normalized, '/');
$segments = array_values(array_filter(explode('/', $normalized), static fn (string $s): bool => $s !== ''));
if ($segments === []) {
throw new \InvalidArgumentException($this->plain('invalid_name_empty_plain'));
}
$classSegment = array_pop($segments);
// Convert to PSR-friendly StudlyCase for both directory segments and class name.
$dirSegments = array_map([$this, 'toStudly'], $segments);
$class = $this->toStudly($classSegment);
$namespace = 'app\\validation';
if ($dirSegments !== []) {
$namespace .= '\\' . implode('\\', $dirSegments);
}
$validationDirName = $this->guessPath(app_path(), 'validation') ?: 'validation';
$baseDir = app_path() . DIRECTORY_SEPARATOR . $validationDirName;
$dir = $dirSegments === []
? $baseDir
: ($baseDir . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $dirSegments));
$file = $dir . DIRECTORY_SEPARATOR . $class . '.php';
return [$namespace, $class, $file];
}
private function toStudly(string $name): string
{
$name = trim($name);
if ($name === '') {
throw new \InvalidArgumentException($this->plain('invalid_segment_empty_plain'));
}
$studly = $this->nameToClass($name);
if (str_contains($studly, '/')) {
// Should never happen because we pass a single segment.
$studly = basename(str_replace('/', DIRECTORY_SEPARATOR, $studly));
}
if (!preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $studly)) {
throw new \InvalidArgumentException($this->plain('invalid_segment_plain', ['{name}' => $name]));
}
return $studly;
}
// Rendering moved to ValidatorGenerator\Support\ValidatorClassRenderer
/**
* @param string $plugin
* @return string relative path
*/
private function getPluginValidationRelativePath(string $plugin): string
{
$plugin = trim($plugin);
$appDir = base_path('plugin' . DIRECTORY_SEPARATOR . $plugin . DIRECTORY_SEPARATOR . 'app');
$validationDir = $this->guessPath($appDir, 'validation') ?: 'validation';
return $this->normalizeRelativePath("plugin/{$plugin}/app/{$validationDir}");
}
/**
* CLI messages (bilingual).
*
* @param string $key
* @param array $replace
* @return string
*/
private function msg(string $key, array $replace = []): string
{
$map = $this->selectMessageMap(Messages::getCliMessages());
$text = $map[$key] ?? $key;
return $replace ? strtr($text, $replace) : $text;
}
/**
* Plain (no console tags) bilingual messages for exception messages.
*
* @param string $key
* @param array $replace
* @return string
*/
private function plain(string $key, array $replace = []): string
{
$map = $this->selectMessageMap(Messages::getPlainMessages());
$text = $map[$key] ?? $key;
return $replace ? strtr($text, $replace) : $text;
}
/**
* Command help text (bilingual).
*
* @return string
*/
private function buildHelpText(): string
{
return $this->selectByLocale(Messages::getHelpText());
}
private function getLocale(): string
{
$locale = 'en';
if (function_exists('config')) {
$value = config('translation.locale', 'en');
$value = is_string($value) ? trim($value) : '';
if ($value !== '') {
$locale = $value;
}
}
return $locale;
}
/**
* @param array<string, string> $localeToValue
*/
private function selectByLocale(array $localeToValue): string
{
$locale = $this->getLocale();
if (isset($localeToValue[$locale])) {
return $localeToValue[$locale];
}
$lang = explode('_', $locale)[0] ?? '';
if ($lang !== '' && isset($localeToValue[$lang])) {
return $localeToValue[$lang];
}
if (isset($localeToValue['en'])) {
return $localeToValue['en'];
}
if (isset($localeToValue['zh_CN'])) {
return $localeToValue['zh_CN'];
}
$first = reset($localeToValue);
return is_string($first) ? $first : '';
}
/**
* @param array<string, array<string, string>> $localeToMessages
* @return array<string, string>
*/
private function selectMessageMap(array $localeToMessages): array
{
$locale = $this->getLocale();
if (isset($localeToMessages[$locale])) {
return $localeToMessages[$locale];
}
$lang = explode('_', $locale)[0] ?? '';
if ($lang !== '' && isset($localeToMessages[$lang])) {
return $localeToMessages[$lang];
}
if (isset($localeToMessages['en'])) {
return $localeToMessages['en'];
}
if (isset($localeToMessages['zh_CN'])) {
return $localeToMessages['zh_CN'];
}
$first = reset($localeToMessages);
return is_array($first) ? $first : [];
}
private function normalizeOptionValue(mixed $value): ?string
{
if ($value === null) {
return null;
}
$value = trim((string)$value);
$value = ltrim($value, '=');
return $value === '' ? null : $value;
}
private function normalizeRelativePath(string $path): string
{
$path = trim($path);
$path = str_replace('\\', '/', $path);
$path = preg_replace('#^\\./+#', '', $path);
$path = trim($path, '/');
return $path;
}
private function isAbsolutePath(string $path): bool
{
$path = trim($path);
if ($path === '') {
return false;
}
if (preg_match('/^[a-zA-Z]:[\\\\\\/]/', $path)) {
return true;
}
if (str_starts_with($path, '\\\\') || str_starts_with($path, '//')) {
return true;
}
return str_starts_with($path, '/') || str_starts_with($path, '\\');
}
private function pathsEqual(string $a, string $b): bool
{
$a = strtolower($this->normalizeRelativePath($a));
$b = strtolower($this->normalizeRelativePath($b));
return $a === $b;
}
private function pluginExists(string $plugin): bool
{
if (!function_exists('config')) {
return false;
}
$cfg = config("plugin.$plugin");
return !empty($cfg);
}
private function resolveDatabaseConnectionNameForTable(
?string $plugin,
?string $explicitConnection,
string $explicitConnectionRaw,
string $orm,
OutputInterface $output
): ?string {
if (!function_exists('config')) {
throw new \RuntimeException($this->plain('config_not_available'));
}
if ($orm === OrmDetector::ORM_THINKORM) {
$main = config('think-orm');
if (!is_array($main) || $main === []) {
$alt = config('thinkorm');
$main = is_array($alt) ? $alt : [];
}
$mainConnections = $main['connections'] ?? null;
$mainConnections = is_array($mainConnections) ? $mainConnections : [];
$mainDefault = $main['default'] ?? null;
$mainDefault = is_string($mainDefault) ? trim($mainDefault) : '';
$usePlugin = false;
$connections = $mainConnections;
$defaultConnection = $mainDefault;
if ($plugin) {
$pluginCfg = config("plugin.$plugin.thinkorm");
if (!is_array($pluginCfg) || $pluginCfg === []) {
$alt = config("plugin.$plugin.think-orm");
$pluginCfg = is_array($alt) ? $alt : [];
}
$pluginConnections = $pluginCfg['connections'] ?? null;
if (is_array($pluginConnections) && $pluginConnections !== []) {
$usePlugin = true;
$connections = $pluginConnections;
$pluginDefault = config("plugin.$plugin.thinkorm.default");
if (!is_string($pluginDefault) || trim($pluginDefault) === '') {
$pluginDefault = config("plugin.$plugin.think-orm.default");
}
$defaultConnection = is_string($pluginDefault) ? trim($pluginDefault) : '';
}
}
$name = $explicitConnection !== null ? trim($explicitConnection) : '';
if ($name === '') {
$name = trim((string)$defaultConnection);
}
if ($name === '') {
throw new \RuntimeException($this->plain('database_connection_not_provided'));
}
if (!array_key_exists($name, $connections)) {
if ($explicitConnection !== null && $explicitConnectionRaw !== '') {
$output->writeln($this->msg('database_connection_not_found', ['{connection}' => $explicitConnectionRaw]));
return null;
}
$available = implode(', ', array_keys($connections));
throw new \RuntimeException($this->plain('thinkorm_connection_not_found', ['{name}' => $name, '{available}' => $available]));
}
return ($usePlugin && $plugin) ? "plugin.$plugin.$name" : $name;
}
$dbConfig = config('database', []);
if (!is_array($dbConfig)) {
throw new \RuntimeException($this->plain('database_config_invalid'));
}
$mainConnections = $dbConfig['connections'] ?? null;
$mainConnections = is_array($mainConnections) ? $mainConnections : [];
$mainDefault = $dbConfig['default'] ?? null;
$mainDefault = is_string($mainDefault) ? trim($mainDefault) : '';
$usePlugin = false;
$connections = $mainConnections;
$defaultConnection = $mainDefault;
if ($plugin) {
$pluginDb = config("plugin.$plugin.database");
if (is_array($pluginDb)) {
$pluginConnections = $pluginDb['connections'] ?? null;
if (is_array($pluginConnections) && $pluginConnections !== []) {
$usePlugin = true;
$connections = $pluginConnections;
$pluginDefault = config("plugin.$plugin.database.default");
$defaultConnection = is_string($pluginDefault) ? trim($pluginDefault) : '';
}
}
}
$name = $explicitConnection !== null ? trim($explicitConnection) : '';
if ($name === '') {
$name = trim((string)$defaultConnection);
}
if ($name === '') {
throw new \RuntimeException($this->plain('database_connection_not_provided'));
}
if (!array_key_exists($name, $connections)) {
if ($explicitConnection !== null && $explicitConnectionRaw !== '') {
$output->writeln($this->msg('database_connection_not_found', ['{connection}' => $explicitConnectionRaw]));
return null;
}
$available = implode(', ', array_keys($connections));
throw new \RuntimeException($this->plain('database_connection_not_found_available', ['{name}' => $name, '{available}' => $available]));
}
return ($usePlugin && $plugin) ? "plugin.$plugin.$name" : $name;
}
private function toRelativePath(string $path): string
{
$base = base_path();
$baseNorm = rtrim(str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $base), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
$pathNorm = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path);
if (str_starts_with(strtolower($pathNorm), strtolower($baseNorm))) {
$rel = substr($pathNorm, strlen($baseNorm));
} else {
$rel = $pathNorm;
}
return str_replace(DIRECTORY_SEPARATOR, '/', $rel);
}
/**
* @return array{0:string,1:string,2:string}|null [class, namespace, file]
*/
private function resolveTargetByPluginOrPath(
string $name,
?string $plugin,
?string $path,
OutputInterface $output,
callable $pluginDefaultPathResolver,
callable $msg
): ?array {
$pathNorm = $path ? $this->normalizeRelativePath($path) : null;
if ($pathNorm !== null && $this->isAbsolutePath($pathNorm)) {
$output->writeln($msg('invalid_path', ['{path}' => (string)$path]));
return null;
}
$expected = null;
if ($plugin) {
$expected = $pluginDefaultPathResolver($plugin);
}
if ($plugin && $pathNorm) {
$pluginPrefix = 'plugin/' . $plugin . '/';
if (!str_starts_with(strtolower($pathNorm), strtolower($pluginPrefix))) {
$output->writeln($msg('plugin_path_conflict', [
'{plugin}' => $plugin,
'{path}' => $pathNorm,
]));
return null;
}
}
$targetRel = $pathNorm ?: $expected;
if (!$targetRel) {
return null;
}
$targetDir = base_path($targetRel);
$namespaceRoot = trim(str_replace('/', '\\', $targetRel), '\\');
if (!($pos = strrpos($name, '/'))) {
$class = ucfirst($name);
$subPath = '';
} else {
$subPath = substr($name, 0, $pos);
$class = ucfirst(substr($name, $pos + 1));
}
$subDir = $subPath ? str_replace('/', DIRECTORY_SEPARATOR, $subPath) . DIRECTORY_SEPARATOR : '';
$file = $targetDir . DIRECTORY_SEPARATOR . $subDir . $class . '.php';
$namespace = $namespaceRoot . ($subPath ? '\\' . str_replace('/', '\\', $subPath) : '');
return [$class, $namespace, $file];
}
private function nameToClass(string $class): string
{
$class = preg_replace_callback(['/-([a-zA-Z])/', '/_([a-zA-Z])/'], function ($matches) {
return strtoupper($matches[1]);
}, $class);
if (!($pos = strrpos($class, '/'))) {
$class = ucfirst($class);
} else {
$path = substr($class, 0, $pos);
$class = ucfirst(substr($class, $pos + 1));
$class = "$path/$class";
}
return $class;
}
private function guessPath(string $basePath, string $name, bool $returnFullPath = false)
{
if (!is_dir($basePath)) {
return false;
}
$names = explode('/', trim(strtolower($name), '/'));
$realname = [];
$path = $basePath;
foreach ($names as $n) {
$found = false;
foreach (scandir($path) ?: [] as $tmpName) {
if (strtolower($tmpName) === $n && is_dir($path . DIRECTORY_SEPARATOR . $tmpName)) {
$path = $path . DIRECTORY_SEPARATOR . $tmpName;
$realname[] = $tmpName;
$found = true;
break;
}
}
if (!$found) {
return false;
}
}
$realname = implode(DIRECTORY_SEPARATOR, $realname);
return $returnFullPath ? realpath($basePath . DIRECTORY_SEPARATOR . $realname) : $realname;
}
}
+936
View File
@@ -0,0 +1,936 @@
<?php
declare(strict_types=1);
namespace Webman\Validation\Command;
/**
* Multi-language messages for make:validator command.
*/
final class Messages
{
/**
* @return array<string, string> locale => description
*/
public static function getDescription(): array
{
return [
'zh_CN' => '生成验证器类',
'zh_TW' => '產生驗證器類',
'en' => 'Create a validator class',
'ja' => 'バリデータークラスを作成',
'ko' => '유효성 검사 클래스 생성',
'fr' => 'Créer une classe de validation',
'de' => 'Validierungsklasse erstellen',
'es' => 'Crear clase de validación',
'pt_BR' => 'Criar classe de validação',
'ru' => 'Создать класс валидации',
'vi' => 'Tạo lớp xác thực',
'tr' => 'Doğrulama sınıfı oluştur',
'id' => 'Buat kelas validasi',
'th' => 'สร้างคลาสตรวจสอบความถูกต้อง',
];
}
/**
* @return array<string, string> locale => argument description
*/
public static function getArgumentName(): array
{
return [
'zh_CN' => '验证器类名(例如:UserValidator、admin/UserValidator',
'zh_TW' => '驗證器類名(例如:UserValidator、admin/UserValidator',
'en' => 'Validator class name (e.g. UserValidator, admin/UserValidator)',
'ja' => 'バリデータークラス名(例:UserValidator、admin/UserValidator',
'ko' => '유효성 검사 클래스 이름 (예: UserValidator, admin/UserValidator)',
'fr' => 'Nom de la classe de validation (ex. UserValidator, admin/UserValidator)',
'de' => 'Name der Validierungsklasse (z. B. UserValidator, admin/UserValidator)',
'es' => 'Nombre de la clase de validación (ej. UserValidator, admin/UserValidator)',
'pt_BR' => 'Nome da classe de validação (ex.: UserValidator, admin/UserValidator)',
'ru' => 'Имя класса валидации (напр. UserValidator, admin/UserValidator)',
'vi' => 'Tên lớp xác thực (vd: UserValidator, admin/UserValidator)',
'tr' => 'Doğrulayıcı sınıf adı (örn. UserValidator, admin/UserValidator)',
'id' => 'Nama kelas validasi (mis. UserValidator, admin/UserValidator)',
'th' => 'ชื่อคลาสตรวจสอบ (เช่น UserValidator, admin/UserValidator)',
];
}
/**
* @return array<string, string> locale => option description
*/
public static function getOptionPlugin(): array
{
return [
'zh_CN' => '插件名(plugin/ 下的目录名),例如:admin',
'zh_TW' => '外掛名稱(plugin/ 下的目錄名),例如:admin',
'en' => 'Plugin name (directory under plugin/). e.g. admin',
'ja' => 'プラグイン名(plugin/ 以下のディレクトリ名)。例:admin',
'ko' => '플러그인 이름 (plugin/ 하위 디렉터리). 예: admin',
'fr' => 'Nom du plugin (répertoire sous plugin/). Ex. : admin',
'de' => 'Plugin-Name (Unterverzeichnis von plugin/). z. B. admin',
'es' => 'Nombre del plugin (directorio bajo plugin/). Ej.: admin',
'pt_BR' => 'Nome do plugin (diretório em plugin/). Ex.: admin',
'ru' => 'Имя плагина (каталог в plugin/). Напр.: admin',
'vi' => 'Tên plugin (thư mục trong plugin/). VD: admin',
'tr' => 'Eklenti adı (plugin/ altındaki dizin). Örn.: admin',
'id' => 'Nama plugin (direktori di bawah plugin/). Mis.: admin',
'th' => 'ชื่อปลั๊กอิน (โฟลเดอร์ใต้ plugin/) เช่น admin',
];
}
/**
* @return array<string, string> locale => option description
*/
public static function getOptionPath(): array
{
return [
'zh_CN' => '目标目录(相对项目根目录),例如:plugin/admin/app/validation',
'zh_TW' => '目標目錄(相對專案根目錄),例如:plugin/admin/app/validation',
'en' => 'Target directory (relative to project root). e.g. plugin/admin/app/validation',
'ja' => '出力先ディレクトリ(プロジェクトルートからの相対パス)。例:plugin/admin/app/validation',
'ko' => '대상 디렉터리 (프로젝트 루트 기준 상대 경로). 예: plugin/admin/app/validation',
'fr' => 'Répertoire cible (relatif à la racine du projet). Ex. : plugin/admin/app/validation',
'de' => 'Zielverzeichnis (relativ zum Projektstamm). z. B. plugin/admin/app/validation',
'es' => 'Directorio destino (relativo a la raíz del proyecto). Ej.: plugin/admin/app/validation',
'pt_BR' => 'Diretório de destino (relativo à raiz do projeto). Ex.: plugin/admin/app/validation',
'ru' => 'Целевой каталог (относительно корня проекта). Напр.: plugin/admin/app/validation',
'vi' => 'Thư mục đích (tương đối so với thư mục gốc dự án). VD: plugin/admin/app/validation',
'tr' => 'Hedef dizin (proje köküne göre). Örn.: plugin/admin/app/validation',
'id' => 'Direktori tujuan (relatif ke root proyek). Mis.: plugin/admin/app/validation',
'th' => 'โฟลเดอร์ปลายทาง (สัมพันธ์กับรากโปรเจกต์) เช่น plugin/admin/app/validation',
];
}
/**
* @return array<string, string> locale => option description
*/
public static function getOptionForce(): array
{
return [
'zh_CN' => '文件已存在时强制覆盖',
'zh_TW' => '檔案已存在時強制覆蓋',
'en' => 'Overwrite if file already exists',
'ja' => 'ファイルが既に存在する場合に上書き',
'ko' => '파일이 이미 있으면 덮어쓰기',
'fr' => 'Écraser si le fichier existe déjà',
'de' => 'Überschreiben, wenn die Datei bereits existiert',
'es' => 'Sobrescribir si el archivo ya existe',
'pt_BR' => 'Sobrescrever se o arquivo já existir',
'ru' => 'Перезаписать, если файл уже существует',
'vi' => 'Ghi đè nếu tệp đã tồn tại',
'tr' => 'Dosya zaten varsa üzerine yaz',
'id' => 'Timpa jika berkas sudah ada',
'th' => 'เขียนทับถ้ามีไฟล์อยู่แล้ว',
];
}
/**
* @return array<string, string> locale => option description
*/
public static function getOptionTable(): array
{
return [
'zh_CN' => '从数据库表推断并生成规则(例如:users)',
'zh_TW' => '從資料庫資料表推斷並產生規則(例如:users)',
'en' => 'Generate rules from database table (e.g. users)',
'ja' => 'データベーステーブルからルールを推論して生成(例:users)',
'ko' => '데이터베이스 테이블에서 규칙 생성 (예: users)',
'fr' => 'Générer les règles à partir d\'une table (ex. : users)',
'de' => 'Regeln aus Datenbanktabelle ableiten (z. B. users)',
'es' => 'Generar reglas desde la tabla de base de datos (ej.: users)',
'pt_BR' => 'Gerar regras a partir da tabela do banco (ex.: users)',
'ru' => 'Сформировать правила по таблице БД (напр. users)',
'vi' => 'Sinh quy tắc từ bảng cơ sở dữ liệu (vd: users)',
'tr' => 'Veritabanı tablosundan kurallar üret (örn.: users)',
'id' => 'Hasilkan aturan dari tabel database (mis. users)',
'th' => 'สร้างกฎจากตารางฐานข้อมูล (เช่น users)',
];
}
/**
* @return array<string, string> locale => option description
*/
public static function getOptionDatabase(): array
{
return [
'zh_CN' => '数据库连接名',
'zh_TW' => '資料庫連線名稱',
'en' => 'Database connection name',
'ja' => 'データベース接続名',
'ko' => '데이터베이스 연결 이름',
'fr' => 'Nom de la connexion à la base de données',
'de' => 'Datenbankverbindungsname',
'es' => 'Nombre de la conexión a la base de datos',
'pt_BR' => 'Nome da conexão do banco de dados',
'ru' => 'Имя подключения к БД',
'vi' => 'Tên kết nối cơ sở dữ liệu',
'tr' => 'Veritabanı bağlantı adı',
'id' => 'Nama koneksi database',
'th' => 'ชื่อการเชื่อมต่อฐานข้อมูล',
];
}
/**
* @return array<string, string> locale => option description
*/
public static function getOptionScenes(): array
{
return [
'zh_CN' => '生成场景(支持:crud',
'zh_TW' => '產生場景(支援:crud',
'en' => 'Generate scenes (supported: crud)',
'ja' => 'シーンを生成(対応:crud',
'ko' => '장면 생성 (지원: crud)',
'fr' => 'Générer des scènes (supporté : crud)',
'de' => 'Szenen erzeugen (unterstützt: crud)',
'es' => 'Generar escenas (soportado: crud)',
'pt_BR' => 'Gerar cenas (suportado: crud)',
'ru' => 'Создать сцены (поддержка: crud)',
'vi' => 'Tạo cảnh (hỗ trợ: crud)',
'tr' => 'Sahneleri oluştur (desteklenen: crud)',
'id' => 'Hasilkan adegan (didukung: crud)',
'th' => 'สร้างฉาก (รองรับ: crud)',
];
}
/**
* @return array<string, string> locale => option description
*/
public static function getOptionOrm(): array
{
return [
'zh_CN' => '使用的 ORMauto|laravel|thinkorm(默认:auto',
'zh_TW' => '使用的 ORMauto|laravel|thinkorm(預設:auto',
'en' => 'ORM to use: auto|laravel|thinkorm (default: auto)',
'ja' => '使用する ORMauto|laravel|thinkorm(既定:auto',
'ko' => '사용할 ORM: auto|laravel|thinkorm (기본: auto)',
'fr' => 'ORM à utiliser : auto|laravel|thinkorm (défaut : auto)',
'de' => 'Zu verwendende ORM: auto|laravel|thinkorm (Standard: auto)',
'es' => 'ORM a usar: auto|laravel|thinkorm (por defecto: auto)',
'pt_BR' => 'ORM a usar: auto|laravel|thinkorm (padrão: auto)',
'ru' => 'ORM: auto|laravel|thinkorm (по умолчанию: auto)',
'vi' => 'ORM sử dụng: auto|laravel|thinkorm (mặc định: auto)',
'tr' => 'Kullanılacak ORM: auto|laravel|thinkorm (varsayılan: auto)',
'id' => 'ORM yang digunakan: auto|laravel|thinkorm (baku: auto)',
'th' => 'ORM ที่ใช้: auto|laravel|thinkorm (ค่าเริ่มต้น: auto)',
];
}
/**
* CLI messages (with Symfony console tags). locale => [ key => text ]
*
* @return array<string, array<string, string>>
*/
public static function getCliMessages(): array
{
return [
'zh_CN' => [
'invalid_name_empty' => '<error>验证器类名不能为空。</error>',
'invalid_name' => '<error>验证器类名无效:{name}</error>',
'invalid_plugin' => '<error>插件名无效:{plugin}。`--plugin/-p` 只能是 plugin/ 目录下的目录名,不能包含 / 或 \\。</error>',
'plugin_not_found' => "<error>插件不存在:</error> <comment>{plugin}</comment>\n请检查插件名是否输入正确,或确认插件已正确安装/启用。",
'plugin_path_conflict' => "<error>`--path/-P` 指定的路径不在 plugin/{plugin}/ 目录下。\n同时使用 `--plugin/-p` 时,`--path/-P` 必须是 plugin/{plugin}/ 下的路径。</error>",
'invalid_path' => '<error>路径无效:{path}。`--path/-P` 必须是相对路径(相对于项目根目录),不能是绝对路径。</error>',
'file_exists' => '<error>文件已存在:</error> {path}',
'override_prompt' => "<question>文件已存在:{path}</question>\n<question>是否覆盖?[Y/n](回车=Y</question>\n",
'use_force' => '使用 <comment>--force/-f</comment> 强制覆盖。',
'scenes_requires_table' => '<error>选项 --scenes 需要同时指定 --table。</error>',
'unsupported_orm' => '<error>不支持的 ORM{orm}(支持:auto/laravel/thinkorm)。</error>',
'database_connection_not_found' => '<error>数据库连接不存在:</error> <comment>{connection}</comment>',
'no_rules_from_table' => '<error>无法从数据表推断出规则:</error> {table}',
'failed_generate_from_table' => '<error>从数据表生成验证器失败:</error> {table}',
'failed_write_file' => '<error>写入文件失败:</error> {path}',
'reason' => '<comment>原因:</comment> {reason}',
'created' => '<info>已创建:</info> {path}',
'class' => '<info>类:</info> {class}',
'table' => '<info>数据表:</info> {table}',
'rules_count' => '<info>规则数:</info> {count}',
'scenes_count' => '<info>场景数:</info> {count}',
],
'zh_TW' => [
'invalid_name_empty' => '<error>驗證器類名不能為空。</error>',
'invalid_name' => '<error>驗證器類名無效:{name}</error>',
'invalid_plugin' => '<error>外掛名稱無效:{plugin}。`--plugin/-p` 只能是 plugin/ 目錄下的目錄名,不能包含 / 或 \\。</error>',
'plugin_not_found' => "<error>外掛不存在:</error> <comment>{plugin}</comment>\n請檢查外掛名稱是否正確,或確認外掛已正確安裝/啟用。",
'plugin_path_conflict' => "<error>`--path/-P` 指定的路徑不在 plugin/{plugin}/ 目錄下。\n同時使用 `--plugin/-p` 時,`--path/-P` 必須是 plugin/{plugin}/ 下的路徑。</error>",
'invalid_path' => '<error>路徑無效:{path}。`--path/-P` 必須是相對路徑(相對於專案根目錄),不能是絕對路徑。</error>',
'file_exists' => '<error>檔案已存在:</error> {path}',
'override_prompt' => "<question>檔案已存在:{path}</question>\n<question>是否覆蓋?[Y/n]Enter=Y</question>\n",
'use_force' => '使用 <comment>--force/-f</comment> 強制覆蓋。',
'scenes_requires_table' => '<error>選項 --scenes 需要同時指定 --table。</error>',
'unsupported_orm' => '<error>不支援的 ORM{orm}(支援:auto/laravel/thinkorm)。</error>',
'database_connection_not_found' => '<error>資料庫連線不存在:</error> <comment>{connection}</comment>',
'no_rules_from_table' => '<error>無法從資料表推斷出規則:</error> {table}',
'failed_generate_from_table' => '<error>從資料表產生驗證器失敗:</error> {table}',
'failed_write_file' => '<error>寫入檔案失敗:</error> {path}',
'reason' => '<comment>原因:</comment> {reason}',
'created' => '<info>已建立:</info> {path}',
'class' => '<info>類別:</info> {class}',
'table' => '<info>資料表:</info> {table}',
'rules_count' => '<info>規則數:</info> {count}',
'scenes_count' => '<info>場景數:</info> {count}',
],
'en' => [
'invalid_name_empty' => '<error>Validator name cannot be empty.</error>',
'invalid_name' => '<error>Invalid validator name: {name}</error>',
'invalid_plugin' => '<error>Invalid plugin name: {plugin}. `--plugin/-p` must be a directory name under plugin/ and must not contain / or \\.</error>',
'plugin_not_found' => "<error>Plugin not found:</error> <comment>{plugin}</comment>\nPlease check the plugin name, or ensure the plugin is properly installed/enabled.",
'plugin_path_conflict' => "<error>`--path/-P` is not under plugin/{plugin}/.\nWhen `--plugin/-p` is specified, `--path/-P` must be under plugin/{plugin}/.</error>",
'invalid_path' => '<error>Invalid path: {path}. `--path/-P` must be a relative path (to project root) and must not be an absolute path.</error>',
'file_exists' => '<error>File already exists:</error> {path}',
'override_prompt' => "<question>File already exists: {path}</question>\n<question>Overwrite? [Y/n] (Enter = Y)</question>\n",
'use_force' => 'Use <comment>--force/-f</comment> to overwrite.',
'scenes_requires_table' => '<error>Option --scenes requires --table.</error>',
'unsupported_orm' => '<error>Unsupported ORM: {orm} (supported: auto/laravel/thinkorm).</error>',
'database_connection_not_found' => '<error>Database connection not found:</error> <comment>{connection}</comment>',
'no_rules_from_table' => '<error>No rules inferred from table:</error> {table}',
'failed_generate_from_table' => '<error>Failed to generate validator from table:</error> {table}',
'failed_write_file' => '<error>Failed to write file:</error> {path}',
'reason' => '<comment>Reason:</comment> {reason}',
'created' => '<info>Created:</info> {path}',
'class' => '<info>Class:</info> {class}',
'table' => '<info>Table:</info> {table}',
'rules_count' => '<info>Rules:</info> {count}',
'scenes_count' => '<info>Scenes:</info> {count}',
],
'ja' => [
'invalid_name_empty' => '<error>バリデーター名を空にできません。</error>',
'invalid_name' => '<error>無効なバリデーター名:{name}</error>',
'invalid_plugin' => '<error>無効なプラグイン名:{plugin}。`--plugin/-p` は plugin/ 以下のディレクトリ名のみ指定でき、/ または \\ を含めません。</error>',
'plugin_not_found' => "<error>プラグインが見つかりません:</error> <comment>{plugin}</comment>\nプラグイン名を確認するか、正しくインストール/有効化されているか確認してください。",
'plugin_path_conflict' => "<error>`--path/-P` が plugin/{plugin}/ 配下にありません。\n`--plugin/-p` 指定時、`--path/-P` は plugin/{plugin}/ 配下のパスでなければなりません。</error>",
'invalid_path' => '<error>無効なパス:{path}。`--path/-P` はプロジェクトルートからの相対パスで、絶対パスは指定できません。</error>',
'file_exists' => '<error>ファイルは既に存在します:</error> {path}',
'override_prompt' => "<question>ファイルは既に存在します:{path}</question>\n<question>上書きしますか?[Y/n]Enter=Y</question>\n",
'use_force' => '<comment>--force/-f</comment> で上書きしてください。',
'scenes_requires_table' => '<error>オプション --scenes には --table の指定が必要です。</error>',
'unsupported_orm' => '<error>サポートされていない ORM{orm}(対応:auto/laravel/thinkorm)。</error>',
'database_connection_not_found' => '<error>データベース接続が見つかりません:</error> <comment>{connection}</comment>',
'no_rules_from_table' => '<error>テーブルからルールを推論できません:</error> {table}',
'failed_generate_from_table' => '<error>テーブルからバリデーターの生成に失敗しました:</error> {table}',
'failed_write_file' => '<error>ファイルの書き込みに失敗しました:</error> {path}',
'reason' => '<comment>理由:</comment> {reason}',
'created' => '<info>作成しました:</info> {path}',
'class' => '<info>クラス:</info> {class}',
'table' => '<info>テーブル:</info> {table}',
'rules_count' => '<info>ルール数:</info> {count}',
'scenes_count' => '<info>シーン数:</info> {count}',
],
'ko' => [
'invalid_name_empty' => '<error>유효성 검사 클래스 이름을 비워 둘 수 없습니다.</error>',
'invalid_name' => '<error>잘못된 유효성 검사 클래스 이름: {name}</error>',
'invalid_plugin' => '<error>잘못된 플러그인 이름: {plugin}. `--plugin/-p`는 plugin/ 아래의 디렉터리 이름만 가능하며 / 또는 \\를 포함할 수 없습니다.</error>',
'plugin_not_found' => "<error>플러그인을 찾을 수 없습니다:</error> <comment>{plugin}</comment>\n플러그인 이름을 확인하거나, 올바르게 설치/활성화되었는지 확인하세요.",
'plugin_path_conflict' => "<error>`--path/-P`가 plugin/{plugin}/ 아래에 없습니다.\n`--plugin/-p` 지정 시 `--path/-P`는 plugin/{plugin}/ 하위 경로여야 합니다.</error>",
'invalid_path' => '<error>잘못된 경로: {path}. `--path/-P`는 프로젝트 루트 기준 상대 경로여야 하며 절대 경로는 사용할 수 없습니다.</error>',
'file_exists' => '<error>파일이 이미 존재합니다:</error> {path}',
'override_prompt' => "<question>파일이 이미 존재합니다: {path}</question>\n<question>덮어쓰시겠습니까? [Y/n] (Enter=Y)</question>\n",
'use_force' => '<comment>--force/-f</comment>로 덮어쓰세요.',
'scenes_requires_table' => '<error>--scenes 옵션에는 --table 지정이 필요합니다.</error>',
'unsupported_orm' => '<error>지원하지 않는 ORM: {orm} (지원: auto/laravel/thinkorm).</error>',
'database_connection_not_found' => '<error>데이터베이스 연결을 찾을 수 없습니다:</error> <comment>{connection}</comment>',
'no_rules_from_table' => '<error>테이블에서 규칙을 추론할 수 없습니다:</error> {table}',
'failed_generate_from_table' => '<error>테이블에서 유효성 검사 클래스 생성에 실패했습니다:</error> {table}',
'failed_write_file' => '<error>파일 쓰기에 실패했습니다:</error> {path}',
'reason' => '<comment>사유:</comment> {reason}',
'created' => '<info>생성됨:</info> {path}',
'class' => '<info>클래스:</info> {class}',
'table' => '<info>테이블:</info> {table}',
'rules_count' => '<info>규칙 수:</info> {count}',
'scenes_count' => '<info>장면 수:</info> {count}',
],
'fr' => [
'invalid_name_empty' => '<error>Le nom du validateur ne peut pas être vide.</error>',
'invalid_name' => '<error>Nom de validateur invalide : {name}</error>',
'invalid_plugin' => '<error>Nom de plugin invalide : {plugin}. `--plugin/-p` doit être un nom de répertoire sous plugin/ et ne doit pas contenir / ou \\.</error>',
'plugin_not_found' => "<error>Plugin introuvable :</error> <comment>{plugin}</comment>\nVérifiez le nom du plugin ou assurez-vous qu'il est correctement installé/activé.",
'plugin_path_conflict' => "<error>`--path/-P` n'est pas sous plugin/{plugin}/.\nAvec `--plugin/-p`, `--path/-P` doit être un chemin sous plugin/{plugin}/.</error>",
'invalid_path' => '<error>Chemin invalide : {path}. `--path/-P` doit être un chemin relatif (à la racine du projet), pas un chemin absolu.</error>',
'file_exists' => '<error>Le fichier existe déjà :</error> {path}',
'override_prompt' => "<question>Le fichier existe déjà : {path}</question>\n<question>Écraser ? [Y/n] (Entrée = Y)</question>\n",
'use_force' => 'Utilisez <comment>--force/-f</comment> pour écraser.',
'scenes_requires_table' => '<error>L\'option --scenes nécessite --table.</error>',
'unsupported_orm' => '<error>ORM non pris en charge : {orm} (pris en charge : auto/laravel/thinkorm).</error>',
'database_connection_not_found' => '<error>Connexion à la base de données introuvable :</error> <comment>{connection}</comment>',
'no_rules_from_table' => '<error>Aucune règle déduite de la table :</error> {table}',
'failed_generate_from_table' => '<error>Échec de la génération du validateur à partir de la table :</error> {table}',
'failed_write_file' => '<error>Échec de l\'écriture du fichier :</error> {path}',
'reason' => '<comment>Raison :</comment> {reason}',
'created' => '<info>Créé :</info> {path}',
'class' => '<info>Classe :</info> {class}',
'table' => '<info>Table :</info> {table}',
'rules_count' => '<info>Règles :</info> {count}',
'scenes_count' => '<info>Scènes :</info> {count}',
],
'de' => [
'invalid_name_empty' => '<error>Der Name des Validators darf nicht leer sein.</error>',
'invalid_name' => '<error>Ungültiger Validator-Name: {name}</error>',
'invalid_plugin' => '<error>Ungültiger Plugin-Name: {plugin}. `--plugin/-p` muss ein Verzeichnisname unter plugin/ sein und darf / oder \\ nicht enthalten.</error>',
'plugin_not_found' => "<error>Plugin nicht gefunden:</error> <comment>{plugin}</comment>\nBitte prüfen Sie den Plugin-Namen oder stellen Sie sicher, dass das Plugin korrekt installiert/aktiviert ist.",
'plugin_path_conflict' => "<error>`--path/-P` liegt nicht unter plugin/{plugin}/.\nBei Angabe von `--plugin/-p` muss `--path/-P` unter plugin/{plugin}/ liegen.</error>",
'invalid_path' => '<error>Ungültiger Pfad: {path}. `--path/-P` muss ein relativer Pfad (zum Projektstamm) sein, kein absoluter Pfad.</error>',
'file_exists' => '<error>Datei existiert bereits:</error> {path}',
'override_prompt' => "<question>Datei existiert bereits: {path}</question>\n<question>Überschreiben? [Y/n] (Eingabe = Y)</question>\n",
'use_force' => 'Mit <comment>--force/-f</comment> überschreiben.',
'scenes_requires_table' => '<error>Option --scenes erfordert --table.</error>',
'unsupported_orm' => '<error>Nicht unterstützte ORM: {orm} (unterstützt: auto/laravel/thinkorm).</error>',
'database_connection_not_found' => '<error>Datenbankverbindung nicht gefunden:</error> <comment>{connection}</comment>',
'no_rules_from_table' => '<error>Keine Regeln aus Tabelle abgeleitet:</error> {table}',
'failed_generate_from_table' => '<error>Validator konnte aus Tabelle nicht erzeugt werden:</error> {table}',
'failed_write_file' => '<error>Datei konnte nicht geschrieben werden:</error> {path}',
'reason' => '<comment>Grund:</comment> {reason}',
'created' => '<info>Erstellt:</info> {path}',
'class' => '<info>Klasse:</info> {class}',
'table' => '<info>Tabelle:</info> {table}',
'rules_count' => '<info>Regeln:</info> {count}',
'scenes_count' => '<info>Szenen:</info> {count}',
],
'es' => [
'invalid_name_empty' => '<error>El nombre del validador no puede estar vacío.</error>',
'invalid_name' => '<error>Nombre de validador no válido: {name}</error>',
'invalid_plugin' => '<error>Nombre de plugin no válido: {plugin}. `--plugin/-p` debe ser un nombre de directorio bajo plugin/ y no puede contener / ni \\.</error>',
'plugin_not_found' => "<error>Plugin no encontrado:</error> <comment>{plugin}</comment>\nCompruebe el nombre del plugin o asegúrese de que está correctamente instalado/habilitado.",
'plugin_path_conflict' => "<error>`--path/-P` no está bajo plugin/{plugin}/.\nAl usar `--plugin/-p`, `--path/-P` debe ser una ruta bajo plugin/{plugin}/.</error>",
'invalid_path' => '<error>Ruta no válida: {path}. `--path/-P` debe ser una ruta relativa (a la raíz del proyecto), no absoluta.</error>',
'file_exists' => '<error>El archivo ya existe:</error> {path}',
'override_prompt' => "<question>El archivo ya existe: {path}</question>\n<question>¿Sobrescribir? [Y/n] (Enter = Y)</question>\n",
'use_force' => 'Use <comment>--force/-f</comment> para sobrescribir.',
'scenes_requires_table' => '<error>La opción --scenes requiere --table.</error>',
'unsupported_orm' => '<error>ORM no soportada: {orm} (soportadas: auto/laravel/thinkorm).</error>',
'database_connection_not_found' => '<error>Conexión a la base de datos no encontrada:</error> <comment>{connection}</comment>',
'no_rules_from_table' => '<error>No se pudieron inferir reglas de la tabla:</error> {table}',
'failed_generate_from_table' => '<error>Error al generar el validador desde la tabla:</error> {table}',
'failed_write_file' => '<error>Error al escribir el archivo:</error> {path}',
'reason' => '<comment>Motivo:</comment> {reason}',
'created' => '<info>Creado:</info> {path}',
'class' => '<info>Clase:</info> {class}',
'table' => '<info>Tabla:</info> {table}',
'rules_count' => '<info>Reglas:</info> {count}',
'scenes_count' => '<info>Escenas:</info> {count}',
],
'pt_BR' => [
'invalid_name_empty' => '<error>O nome do validador não pode estar vazio.</error>',
'invalid_name' => '<error>Nome de validador inválido: {name}</error>',
'invalid_plugin' => '<error>Nome de plugin inválido: {plugin}. `--plugin/-p` deve ser um nome de diretório em plugin/ e não pode conter / ou \\.</error>',
'plugin_not_found' => "<error>Plugin não encontrado:</error> <comment>{plugin}</comment>\nVerifique o nome do plugin ou confira se está instalado/ativado corretamente.",
'plugin_path_conflict' => "<error>`--path/-P` não está em plugin/{plugin}/.\nAo usar `--plugin/-p`, `--path/-P` deve estar sob plugin/{plugin}/.</error>",
'invalid_path' => '<error>Caminho inválido: {path}. `--path/-P` deve ser um caminho relativo (à raiz do projeto), não absoluto.</error>',
'file_exists' => '<error>O arquivo já existe:</error> {path}',
'override_prompt' => "<question>O arquivo já existe: {path}</question>\n<question>Sobrescrever? [Y/n] (Enter = Y)</question>\n",
'use_force' => 'Use <comment>--force/-f</comment> para sobrescrever.',
'scenes_requires_table' => '<error>A opção --scenes exige --table.</error>',
'unsupported_orm' => '<error>ORM não suportada: {orm} (suportadas: auto/laravel/thinkorm).</error>',
'database_connection_not_found' => '<error>Conexão com o banco não encontrada:</error> <comment>{connection}</comment>',
'no_rules_from_table' => '<error>Nenhuma regra inferida da tabela:</error> {table}',
'failed_generate_from_table' => '<error>Falha ao gerar validador a partir da tabela:</error> {table}',
'failed_write_file' => '<error>Falha ao escrever o arquivo:</error> {path}',
'reason' => '<comment>Motivo:</comment> {reason}',
'created' => '<info>Criado:</info> {path}',
'class' => '<info>Classe:</info> {class}',
'table' => '<info>Tabela:</info> {table}',
'rules_count' => '<info>Regras:</info> {count}',
'scenes_count' => '<info>Cenas:</info> {count}',
],
'ru' => [
'invalid_name_empty' => '<error>Имя класса валидации не может быть пустым.</error>',
'invalid_name' => '<error>Недопустимое имя валидатора: {name}</error>',
'invalid_plugin' => '<error>Недопустимое имя плагина: {plugin}. Для `--plugin/-p` допустимо только имя каталога в plugin/, без / и \\.</error>',
'plugin_not_found' => "<error>Плагин не найден:</error> <comment>{plugin}</comment>\nПроверьте имя плагина или убедитесь, что он установлен и включён.",
'plugin_path_conflict' => "<error>`--path/-P` не находится в plugin/{plugin}/.\nПри использовании `--plugin/-p` `--path/-P` должен быть путём внутри plugin/{plugin}/.</error>",
'invalid_path' => '<error>Недопустимый путь: {path}. Для `--path/-P` нужен относительный путь (от корня проекта), не абсолютный.</error>',
'file_exists' => '<error>Файл уже существует:</error> {path}',
'override_prompt' => "<question>Файл уже существует: {path}</question>\n<question>Перезаписать? [Y/n] (Enter = Y)</question>\n",
'use_force' => 'Используйте <comment>--force/-f</comment> для перезаписи.',
'scenes_requires_table' => '<error>Для опции --scenes необходимо указать --table.</error>',
'unsupported_orm' => '<error>Неподдерживаемая ORM: {orm} (поддерживаются: auto/laravel/thinkorm).</error>',
'database_connection_not_found' => '<error>Подключение к БД не найдено:</error> <comment>{connection}</comment>',
'no_rules_from_table' => '<error>Не удалось вывести правила по таблице:</error> {table}',
'failed_generate_from_table' => '<error>Не удалось сформировать валидатор по таблице:</error> {table}',
'failed_write_file' => '<error>Не удалось записать файл:</error> {path}',
'reason' => '<comment>Причина:</comment> {reason}',
'created' => '<info>Создано:</info> {path}',
'class' => '<info>Класс:</info> {class}',
'table' => '<info>Таблица:</info> {table}',
'rules_count' => '<info>Правил:</info> {count}',
'scenes_count' => '<info>Сцен:</info> {count}',
],
'vi' => [
'invalid_name_empty' => '<error>Tên lớp xác thực không được để trống.</error>',
'invalid_name' => '<error>Tên lớp xác thực không hợp lệ: {name}</error>',
'invalid_plugin' => '<error>Tên plugin không hợp lệ: {plugin}. `--plugin/-p` phải là tên thư mục trong plugin/ và không được chứa / hoặc \\.</error>',
'plugin_not_found' => "<error>Không tìm thấy plugin:</error> <comment>{plugin}</comment>\nVui lòng kiểm tra tên plugin hoặc đảm bảo plugin đã được cài đặt/bật đúng cách.",
'plugin_path_conflict' => "<error>`--path/-P` không nằm trong plugin/{plugin}/.\nKhi dùng `--plugin/-p`, `--path/-P` phải là đường dẫn trong plugin/{plugin}/.</error>",
'invalid_path' => '<error>Đường dẫn không hợp lệ: {path}. `--path/-P` phải là đường dẫn tương đối (so với thư mục gốc dự án), không phải đường dẫn tuyệt đối.</error>',
'file_exists' => '<error>Tệp đã tồn tại:</error> {path}',
'override_prompt' => "<question>Tệp đã tồn tại: {path}</question>\n<question>Ghi đè? [Y/n] (Enter = Y)</question>\n",
'use_force' => 'Dùng <comment>--force/-f</comment> để ghi đè.',
'scenes_requires_table' => '<error>Tùy chọn --scenes yêu cầu --table.</error>',
'unsupported_orm' => '<error>ORM không được hỗ trợ: {orm} (hỗ trợ: auto/laravel/thinkorm).</error>',
'database_connection_not_found' => '<error>Không tìm thấy kết nối cơ sở dữ liệu:</error> <comment>{connection}</comment>',
'no_rules_from_table' => '<error>Không suy ra được quy tắc từ bảng:</error> {table}',
'failed_generate_from_table' => '<error>Không thể tạo lớp xác thực từ bảng:</error> {table}',
'failed_write_file' => '<error>Không thể ghi tệp:</error> {path}',
'reason' => '<comment>Lý do:</comment> {reason}',
'created' => '<info>Đã tạo:</info> {path}',
'class' => '<info>Lớp:</info> {class}',
'table' => '<info>Bảng:</info> {table}',
'rules_count' => '<info>Số quy tắc:</info> {count}',
'scenes_count' => '<info>Số cảnh:</info> {count}',
],
'tr' => [
'invalid_name_empty' => '<error>Doğrulayıcı adı boş bırakılamaz.</error>',
'invalid_name' => '<error>Geçersiz doğrulayıcı adı: {name}</error>',
'invalid_plugin' => '<error>Geçersiz eklenti adı: {plugin}. `--plugin/-p` yalnızca plugin/ altındaki bir dizin adı olmalı, / veya \\ içeremez.</error>',
'plugin_not_found' => "<error>Eklenti bulunamadı:</error> <comment>{plugin}</comment>\nEklenti adını kontrol edin veya doğru kurulduğundan/etkinleştirildiğinden emin olun.",
'plugin_path_conflict' => "<error>`--path/-P` plugin/{plugin}/ altında değil.\n`--plugin/-p` belirtildiğinde `--path/-P` plugin/{plugin}/ altında bir yol olmalıdır.</error>",
'invalid_path' => '<error>Geçersiz yol: {path}. `--path/-P` proje köküne göre göreli yol olmalı, mutlak yol olmamalı.</error>',
'file_exists' => '<error>Dosya zaten mevcut:</error> {path}',
'override_prompt' => "<question>Dosya zaten mevcut: {path}</question>\n<question>Üzerine yazılsın mı? [Y/n] (Enter = Y)</question>\n",
'use_force' => 'Üzerine yazmak için <comment>--force/-f</comment> kullanın.',
'scenes_requires_table' => '<error>--scenes seçeneği --table gerektirir.</error>',
'unsupported_orm' => '<error>Desteklenmeyen ORM: {orm} (desteklenen: auto/laravel/thinkorm).</error>',
'database_connection_not_found' => '<error>Veritabanı bağlantısı bulunamadı:</error> <comment>{connection}</comment>',
'no_rules_from_table' => '<error>Tablodan kural çıkarılamadı:</error> {table}',
'failed_generate_from_table' => '<error>Tablodan doğrulayıcı oluşturulamadı:</error> {table}',
'failed_write_file' => '<error>Dosya yazılamadı:</error> {path}',
'reason' => '<comment>Neden:</comment> {reason}',
'created' => '<info>Oluşturuldu:</info> {path}',
'class' => '<info>Sınıf:</info> {class}',
'table' => '<info>Tablo:</info> {table}',
'rules_count' => '<info>Kural sayısı:</info> {count}',
'scenes_count' => '<info>Sahne sayısı:</info> {count}',
],
'id' => [
'invalid_name_empty' => '<error>Nama validator tidak boleh kosong.</error>',
'invalid_name' => '<error>Nama validator tidak valid: {name}</error>',
'invalid_plugin' => '<error>Nama plugin tidak valid: {plugin}. `--plugin/-p` harus nama direktori di bawah plugin/ dan tidak boleh berisi / atau \\.</error>',
'plugin_not_found' => "<error>Plugin tidak ditemukan:</error> <comment>{plugin}</comment>\nPeriksa nama plugin atau pastikan plugin terpasang/diaktifkan dengan benar.",
'plugin_path_conflict' => "<error>`--path/-P` tidak berada di bawah plugin/{plugin}/.\nSaat `--plugin/-p` ditentukan, `--path/-P` harus di bawah plugin/{plugin}/.</error>",
'invalid_path' => '<error>Path tidak valid: {path}. `--path/-P` harus path relatif (ke root proyek), bukan path absolut.</error>',
'file_exists' => '<error>Berkas sudah ada:</error> {path}',
'override_prompt' => "<question>Berkas sudah ada: {path}</question>\n<question>Timpa? [Y/n] (Enter = Y)</question>\n",
'use_force' => 'Gunakan <comment>--force/-f</comment> untuk menimpa.',
'scenes_requires_table' => '<error>Opsi --scenes memerlukan --table.</error>',
'unsupported_orm' => '<error>ORM tidak didukung: {orm} (didukung: auto/laravel/thinkorm).</error>',
'database_connection_not_found' => '<error>Koneksi database tidak ditemukan:</error> <comment>{connection}</comment>',
'no_rules_from_table' => '<error>Tidak ada aturan yang disimpulkan dari tabel:</error> {table}',
'failed_generate_from_table' => '<error>Gagal membuat validator dari tabel:</error> {table}',
'failed_write_file' => '<error>Gagal menulis berkas:</error> {path}',
'reason' => '<comment>Alasan:</comment> {reason}',
'created' => '<info>Dibuat:</info> {path}',
'class' => '<info>Kelas:</info> {class}',
'table' => '<info>Tabel:</info> {table}',
'rules_count' => '<info>Jumlah aturan:</info> {count}',
'scenes_count' => '<info>Jumlah adegan:</info> {count}',
],
'th' => [
'invalid_name_empty' => '<error>ชื่อคลาสตรวจสอบไม่สามารถเว้นว่างได้</error>',
'invalid_name' => '<error>ชื่อคลาสตรวจสอบไม่ถูกต้อง: {name}</error>',
'invalid_plugin' => '<error>ชื่อปลั๊กอินไม่ถูกต้อง: {plugin} สำหรับ `--plugin/-p` ต้องเป็นชื่อโฟลเดอร์ภายใต้ plugin/ และห้ามมี / หรือ \\</error>',
'plugin_not_found' => "<error>ไม่พบปลั๊กอิน:</error> <comment>{plugin}</comment>\nกรุณาตรวจสอบชื่อปลั๊กอิน หรือตรวจสอบว่าติดตั้ง/เปิดใช้แล้วอย่างถูกต้อง",
'plugin_path_conflict' => "<error>`--path/-P` ไม่อยู่ภายใต้ plugin/{plugin}/\nเมื่อระบุ `--plugin/-p` `--path/-P` ต้องเป็นเส้นทางภายใต้ plugin/{plugin}/</error>",
'invalid_path' => '<error>เส้นทางไม่ถูกต้อง: {path} สำหรับ `--path/-P` ต้องเป็นเส้นทางสัมพัทธ์ (จากรากโปรเจกต์) ไม่ใช่เส้นทางสัมบูรณ์</error>',
'file_exists' => '<error>มีไฟล์อยู่แล้ว:</error> {path}',
'override_prompt' => "<question>มีไฟล์อยู่แล้ว: {path}</question>\n<question>เขียนทับหรือไม่? [Y/n] (Enter = Y)</question>\n",
'use_force' => 'ใช้ <comment>--force/-f</comment> เพื่อเขียนทับ',
'scenes_requires_table' => '<error>ตัวเลือก --scenes ต้องใช้ร่วมกับ --table</error>',
'unsupported_orm' => '<error>ไม่รองรับ ORM: {orm} (รองรับ: auto/laravel/thinkorm)</error>',
'database_connection_not_found' => '<error>ไม่พบการเชื่อมต่อฐานข้อมูล:</error> <comment>{connection}</comment>',
'no_rules_from_table' => '<error>ไม่สามารถสรุปกฎจากตารางได้:</error> {table}',
'failed_generate_from_table' => '<error>สร้างคลาสตรวจสอบจากตารางไม่สำเร็จ:</error> {table}',
'failed_write_file' => '<error>เขียนไฟล์ไม่สำเร็จ:</error> {path}',
'reason' => '<comment>สาเหตุ:</comment> {reason}',
'created' => '<info>สร้างแล้ว:</info> {path}',
'class' => '<info>คลาส:</info> {class}',
'table' => '<info>ตาราง:</info> {table}',
'rules_count' => '<info>จำนวนกฎ:</info> {count}',
'scenes_count' => '<info>จำนวนฉาก:</info> {count}',
],
];
}
/**
* Plain (no console tags) messages for exception messages. locale => [ key => text ]
*
* @return array<string, array<string, string>>
*/
public static function getPlainMessages(): array
{
return [
'zh_CN' => [
'invalid_name_empty_plain' => '验证器类名不能为空。',
'invalid_segment_empty_plain' => '类名段不能为空。',
'invalid_segment_plain' => '类名段无效:{name}',
'config_not_available' => '配置不可用。',
'database_connection_not_provided' => '未提供数据库连接名。',
'thinkorm_connection_not_found' => 'ThinkORM 连接不存在:{name}(可用:{available})。',
'database_config_invalid' => '数据库配置无效。',
'database_connection_not_found_available' => '数据库连接不存在:{name}(可用:{available})。',
],
'zh_TW' => [
'invalid_name_empty_plain' => '驗證器類名不能為空。',
'invalid_segment_empty_plain' => '類名段不能為空。',
'invalid_segment_plain' => '類名段無效:{name}',
'config_not_available' => '配置不可用。',
'database_connection_not_provided' => '未提供資料庫連線名稱。',
'thinkorm_connection_not_found' => 'ThinkORM 連線不存在:{name}(可用:{available})。',
'database_config_invalid' => '資料庫配置無效。',
'database_connection_not_found_available' => '資料庫連線不存在:{name}(可用:{available})。',
],
'en' => [
'invalid_name_empty_plain' => 'Validator class name cannot be empty.',
'invalid_segment_empty_plain' => 'Class name segment cannot be empty.',
'invalid_segment_plain' => 'Invalid class name segment: {name}',
'config_not_available' => 'Configuration is not available.',
'database_connection_not_provided' => 'Database connection name not provided.',
'thinkorm_connection_not_found' => 'ThinkORM connection not found: {name} (available: {available}).',
'database_config_invalid' => 'Database configuration is invalid.',
'database_connection_not_found_available' => 'Database connection not found: {name} (available: {available}).',
],
'ja' => [
'invalid_name_empty_plain' => 'バリデータークラス名を空にできません。',
'invalid_segment_empty_plain' => 'クラス名セグメントを空にできません。',
'invalid_segment_plain' => '無効なクラス名セグメント:{name}',
'config_not_available' => '設定が利用できません。',
'database_connection_not_provided' => 'データベース接続名が提供されていません。',
'thinkorm_connection_not_found' => 'ThinkORM 接続が見つかりません:{name}(利用可能:{available})。',
'database_config_invalid' => 'データベース設定が無効です。',
'database_connection_not_found_available' => 'データベース接続が見つかりません:{name}(利用可能:{available})。',
],
'ko' => [
'invalid_name_empty_plain' => '유효성 검사 클래스 이름을 비워 둘 수 없습니다.',
'invalid_segment_empty_plain' => '클래스 이름 세그먼트를 비워 둘 수 없습니다.',
'invalid_segment_plain' => '잘못된 클래스 이름 세그먼트: {name}',
'config_not_available' => '구성이 사용할 수 없습니다.',
'database_connection_not_provided' => '데이터베이스 연결 이름이 제공되지 않았습니다.',
'thinkorm_connection_not_found' => 'ThinkORM 연결을 찾을 수 없습니다: {name} (사용 가능: {available}).',
'database_config_invalid' => '데이터베이스 구성이 유효하지 않습니다.',
'database_connection_not_found_available' => '데이터베이스 연결을 찾을 수 없습니다: {name} (사용 가능: {available}).',
],
'fr' => [
'invalid_name_empty_plain' => 'Le nom de la classe de validation ne peut pas être vide.',
'invalid_segment_empty_plain' => 'Le segment du nom de classe ne peut pas être vide.',
'invalid_segment_plain' => 'Segment de nom de classe invalide : {name}',
'config_not_available' => 'La configuration n\'est pas disponible.',
'database_connection_not_provided' => 'Nom de connexion à la base de données non fourni.',
'thinkorm_connection_not_found' => 'Connexion ThinkORM introuvable : {name} (disponible : {available}).',
'database_config_invalid' => 'La configuration de la base de données est invalide.',
'database_connection_not_found_available' => 'Connexion à la base de données introuvable : {name} (disponible : {available}).',
],
'de' => [
'invalid_name_empty_plain' => 'Der Name der Validierungsklasse darf nicht leer sein.',
'invalid_segment_empty_plain' => 'Klassennamensegment darf nicht leer sein.',
'invalid_segment_plain' => 'Ungültiges Klassennamensegment: {name}',
'config_not_available' => 'Konfiguration ist nicht verfügbar.',
'database_connection_not_provided' => 'Datenbankverbindungsname nicht angegeben.',
'thinkorm_connection_not_found' => 'ThinkORM-Verbindung nicht gefunden: {name} (verfügbar: {available}).',
'database_config_invalid' => 'Datenbankkonfiguration ist ungültig.',
'database_connection_not_found_available' => 'Datenbankverbindung nicht gefunden: {name} (verfügbar: {available}).',
],
'es' => [
'invalid_name_empty_plain' => 'El nombre de la clase de validación no puede estar vacío.',
'invalid_segment_empty_plain' => 'El segmento del nombre de clase no puede estar vacío.',
'invalid_segment_plain' => 'Segmento de nombre de clase no válido: {name}',
'config_not_available' => 'La configuración no está disponible.',
'database_connection_not_provided' => 'Nombre de conexión a la base de datos no proporcionado.',
'thinkorm_connection_not_found' => 'Conexión ThinkORM no encontrada: {name} (disponible: {available}).',
'database_config_invalid' => 'La configuración de la base de datos es inválida.',
'database_connection_not_found_available' => 'Conexión a la base de datos no encontrada: {name} (disponible: {available}).',
],
'pt_BR' => [
'invalid_name_empty_plain' => 'O nome da classe de validação não pode estar vazio.',
'invalid_segment_empty_plain' => 'O segmento do nome da classe não pode estar vazio.',
'invalid_segment_plain' => 'Segmento de nome de classe inválido: {name}',
'config_not_available' => 'A configuração não está disponível.',
'database_connection_not_provided' => 'Nome da conexão do banco de dados não fornecido.',
'thinkorm_connection_not_found' => 'Conexão ThinkORM não encontrada: {name} (disponível: {available}).',
'database_config_invalid' => 'A configuração do banco de dados é inválida.',
'database_connection_not_found_available' => 'Conexão com o banco de dados não encontrada: {name} (disponível: {available}).',
],
'ru' => [
'invalid_name_empty_plain' => 'Имя класса валидации не может быть пустым.',
'invalid_segment_empty_plain' => 'Сегмент имени класса не может быть пустым.',
'invalid_segment_plain' => 'Недопустимый сегмент имени класса: {name}',
'config_not_available' => 'Конфигурация недоступна.',
'database_connection_not_provided' => 'Имя подключения к БД не указано.',
'thinkorm_connection_not_found' => 'Подключение ThinkORM не найдено: {name} (доступно: {available}).',
'database_config_invalid' => 'Конфигурация БД недействительна.',
'database_connection_not_found_available' => 'Подключение к БД не найдено: {name} (доступно: {available}).',
],
'vi' => [
'invalid_name_empty_plain' => 'Tên lớp xác thực không được để trống.',
'invalid_segment_empty_plain' => 'Đoạn tên lớp không được để trống.',
'invalid_segment_plain' => 'Đoạn tên lớp không hợp lệ: {name}',
'config_not_available' => 'Cấu hình không khả dụng.',
'database_connection_not_provided' => 'Tên kết nối cơ sở dữ liệu chưa được cung cấp.',
'thinkorm_connection_not_found' => 'Không tìm thấy kết nối ThinkORM: {name} (có sẵn: {available}).',
'database_config_invalid' => 'Cấu hình cơ sở dữ liệu không hợp lệ.',
'database_connection_not_found_available' => 'Không tìm thấy kết nối cơ sở dữ liệu: {name} (có sẵn: {available}).',
],
'tr' => [
'invalid_name_empty_plain' => 'Doğrulama sınıf adı boş bırakılamaz.',
'invalid_segment_empty_plain' => 'Sınıf adı segmenti boş bırakılamaz.',
'invalid_segment_plain' => 'Geçersiz sınıf adı segmenti: {name}',
'config_not_available' => 'Yapılandırma kullanılamıyor.',
'database_connection_not_provided' => 'Veritabanı bağlantı adı sağlanmadı.',
'thinkorm_connection_not_found' => 'ThinkORM bağlantısı bulunamadı: {name} (mevcut: {available}).',
'database_config_invalid' => 'Veritabanı yapılandırması geçersiz.',
'database_connection_not_found_available' => 'Veritabanı bağlantısı bulunamadı: {name} (mevcut: {available}).',
],
'id' => [
'invalid_name_empty_plain' => 'Nama kelas validasi tidak boleh kosong.',
'invalid_segment_empty_plain' => 'Segmen nama kelas tidak boleh kosong.',
'invalid_segment_plain' => 'Segmen nama kelas tidak valid: {name}',
'config_not_available' => 'Konfigurasi tidak tersedia.',
'database_connection_not_provided' => 'Nama koneksi database tidak diberikan.',
'thinkorm_connection_not_found' => 'Koneksi ThinkORM tidak ditemukan: {name} (tersedia: {available}).',
'database_config_invalid' => 'Konfigurasi database tidak valid.',
'database_connection_not_found_available' => 'Koneksi database tidak ditemukan: {name} (tersedia: {available}).',
],
'th' => [
'invalid_name_empty_plain' => 'ชื่อคลาสตรวจสอบไม่สามารถเว้นว่างได้',
'invalid_segment_empty_plain' => 'ส่วนชื่อคลาสไม่สามารถเว้นว่างได้',
'invalid_segment_plain' => 'ส่วนชื่อคลาสไม่ถูกต้อง: {name}',
'config_not_available' => 'การกำหนดค่าไม่พร้อมใช้งาน',
'database_connection_not_provided' => 'ไม่ได้ระบุชื่อการเชื่อมต่อฐานข้อมูล',
'thinkorm_connection_not_found' => 'ไม่พบการเชื่อมต่อ ThinkORM: {name} (มีให้: {available})',
'database_config_invalid' => 'การกำหนดค่าฐานข้อมูลไม่ถูกต้อง',
'database_connection_not_found_available' => 'ไม่พบการเชื่อมต่อฐานข้อมูล: {name} (มีให้: {available})',
],
];
}
/**
* Command help text (bilingual).
*
* @return array<string, string> locale => help text
*/
public static function getHelpText(): array
{
return [
'zh_CN' => <<<'EOF'
生成验证器类文件(默认在 app/validation 下)。
推荐用法:
php webman make:validator UserValidator
php webman make:validator admin/UserValidator
php webman make:validator UserValidator -p admin
php webman make:validator UserValidator -P plugin/admin/app/validation
说明:
- 默认生成到 app/validation(大小写以现有目录为准)。
- 使用 -p/--plugin 时默认生成到 plugin/<plugin>/app/validation。
- 使用 -P/--path 时生成到指定相对目录(相对于项目根目录)。
- 文件已存在时默认拒绝覆盖;使用 -f/--force 可强制覆盖。
- 使用 -t/--table 可从数据库数据表推断规则;如需生成场景请同时指定 -s/--scenes(例如 crud)。
EOF,
'zh_TW' => <<<'EOF'
產生驗證器類檔案(預設在 app/validation 下)。
推薦用法:
php webman make:validator UserValidator
php webman make:validator admin/UserValidator
php webman make:validator UserValidator -p admin
php webman make:validator UserValidator -P plugin/admin/app/validation
說明:
- 預設產生到 app/validation(大小寫以現有目錄為準)。
- 使用 -p/--plugin 時預設產生到 plugin/<plugin>/app/validation。
- 使用 -P/--path 時產生到指定相對目錄(相對於專案根目錄)。
- 檔案已存在時預設拒絕覆蓋;使用 -f/--force 可強制覆蓋。
- 使用 -t/--table 可從資料庫資料表推斷規則;如需產生場景請同時指定 -s/--scenes(例如 crud)。
EOF,
'en' => <<<'EOF'
Generate a validator class file (default under app/validation).
Recommended:
php webman make:validator UserValidator
php webman make:validator admin/UserValidator
php webman make:validator UserValidator -p admin
php webman make:validator UserValidator -P plugin/admin/app/validation
Notes:
- By default, it generates under app/validation (case depends on existing directory).
- With -p/--plugin, it generates under plugin/<plugin>/app/validation by default.
- With -P/--path, it generates under the specified relative directory (to project root).
- If the file already exists, it refuses to overwrite by default; use -f/--force to overwrite.
- With -t/--table, it infers rules from a database table; to generate scenes, also provide -s/--scenes (e.g. crud).
EOF,
'ja' => <<<'EOF'
バリデータークラスファイルを生成します(デフォルトは app/validation 以下)。
推奨用法:
php webman make:validator UserValidator
php webman make:validator admin/UserValidator
php webman make:validator UserValidator -p admin
php webman make:validator UserValidator -P plugin/admin/app/validation
説明:
- デフォルトでは app/validation 以下に生成(大文字小文字は既存ディレクトリに合わせます)。
- -p/--plugin 使用時は plugin/<plugin>/app/validation 以下に生成。
- -P/--path 使用時は指定した相対ディレクトリ(プロジェクトルート基準)に生成。
- ファイルが既に存在する場合はデフォルトで上書きしません。-f/--force で上書きできます。
- -t/--table でデータベーステーブルからルールを推論。シーンも生成する場合は -s/--scenes(例:crud)を指定してください。
EOF,
'ko' => <<<'EOF'
유효성 검사 클래스 파일을 생성합니다 (기본 위치: app/validation).
권장 사용법:
php webman make:validator UserValidator
php webman make:validator admin/UserValidator
php webman make:validator UserValidator -p admin
php webman make:validator UserValidator -P plugin/admin/app/validation
설명:
- 기본적으로 app/validation 아래에 생성합니다 (대소문자는 기존 디렉터리에 맞춤).
- -p/--plugin 사용 plugin/<plugin>/app/validation 아래에 생성합니다.
- -P/--path 사용 지정한 상대 디렉터리(프로젝트 루트 기준) 생성합니다.
- 파일이 이미 있으면 기본적으로 덮어쓰지 않습니다. -f/--force로 덮어쓸 있습니다.
- -t/--table로 데이터베이스 테이블에서 규칙을 추론합니다. 장면도 생성하려면 -s/--scenes(: crud) 함께 지정하세요.
EOF,
'fr' => <<<'EOF'
Génère un fichier de classe de validation (par défaut sous app/validation).
Recommandé :
php webman make:validator UserValidator
php webman make:validator admin/UserValidator
php webman make:validator UserValidator -p admin
php webman make:validator UserValidator -P plugin/admin/app/validation
Notes :
- Par défaut, génération sous app/validation (casse selon le répertoire existant).
- Avec -p/--plugin, génération sous plugin/<plugin>/app/validation par défaut.
- Avec -P/--path, génération dans le répertoire relatif indiqué (par rapport à la racine du projet).
- Si le fichier existe déjà, refus d'écraser par défaut ; utilisez -f/--force pour écraser.
- Avec -t/--table, déduction des règles à partir d'une table ; pour générer des scènes, indiquez aussi -s/--scenes (ex. crud).
EOF,
'de' => <<<'EOF'
Erstellt eine Validierungsklassen-Datei (Standard: unter app/validation).
Empfohlen:
php webman make:validator UserValidator
php webman make:validator admin/UserValidator
php webman make:validator UserValidator -p admin
php webman make:validator UserValidator -P plugin/admin/app/validation
Hinweise:
- Standardmäßig wird unter app/validation erzeugt (Groß-/Kleinschreibung nach vorhandenem Verzeichnis).
- Mit -p/--plugin wird standardmäßig unter plugin/<plugin>/app/validation erzeugt.
- Mit -P/--path wird im angegebenen relativen Verzeichnis (zum Projektstamm) erzeugt.
- Wenn die Datei bereits existiert, wird standardmäßig nicht überschrieben; mit -f/--force erzwingen.
- Mit -t/--table werden Regeln aus einer Datenbanktabelle abgeleitet; für Szenen zusätzlich -s/--scenes angeben (z. B. crud).
EOF,
'es' => <<<'EOF'
Genera un archivo de clase de validación (por defecto bajo app/validation).
Recomendado:
php webman make:validator UserValidator
php webman make:validator admin/UserValidator
php webman make:validator UserValidator -p admin
php webman make:validator UserValidator -P plugin/admin/app/validation
Notas:
- Por defecto genera bajo app/validation (mayúsculas/minúsculas según el directorio existente).
- Con -p/--plugin genera bajo plugin/<plugin>/app/validation por defecto.
- Con -P/--path genera en el directorio relativo indicado (respecto a la raíz del proyecto).
- Si el archivo ya existe, por defecto no se sobrescribe; use -f/--force para sobrescribir.
- Con -t/--table infiere reglas desde una tabla; para generar escenas, indique también -s/--scenes (ej. crud).
EOF,
'pt_BR' => <<<'EOF'
Gera um arquivo de classe de validação (padrão em app/validation).
Recomendado:
php webman make:validator UserValidator
php webman make:validator admin/UserValidator
php webman make:validator UserValidator -p admin
php webman make:validator UserValidator -P plugin/admin/app/validation
Notas:
- Por padrão gera em app/validation (maiúsculas/minúsculas conforme o diretório existente).
- Com -p/--plugin gera em plugin/<plugin>/app/validation por padrão.
- Com -P/--path gera no diretório relativo informado (em relação à raiz do projeto).
- Se o arquivo existir, por padrão não sobrescreve; use -f/--force para sobrescrever.
- Com -t/--table infere regras a partir de uma tabela; para gerar cenas, informe também -s/--scenes (ex.: crud).
EOF,
'ru' => <<<'EOF'
Создаёт файл класса валидации (по умолчанию в app/validation).
Рекомендуется:
php webman make:validator UserValidator
php webman make:validator admin/UserValidator
php webman make:validator UserValidator -p admin
php webman make:validator UserValidator -P plugin/admin/app/validation
Примечания:
- По умолчанию создаётся в app/validation (регистр по существующей папке).
- С -p/--plugin по умолчанию создаётся в plugin/<plugin>/app/validation.
- С -P/--path создаётся в указанной относительной директории (от корня проекта).
- Если файл уже есть, по умолчанию не перезаписывается; используйте -f/--force для перезаписи.
- С -t/--table правила выводятся из таблицы БД; для сцен укажите также -s/--scenes (напр. crud).
EOF,
'vi' => <<<'EOF'
Tạo tệp lớp xác thực (mặc định trong app/validation).
Khuyến nghị:
php webman make:validator UserValidator
php webman make:validator admin/UserValidator
php webman make:validator UserValidator -p admin
php webman make:validator UserValidator -P plugin/admin/app/validation
Ghi chú:
- Mặc định tạo trong app/validation (chữ hoa/thường theo thư mục hiện ).
- Dùng -p/--plugin thì mặc định tạo trong plugin/<plugin>/app/validation.
- Dùng -P/--path thì tạo vào thư mục tương đối chỉ định (so với thư mục gốc dự án).
- Nếu tệp đã tồn tại thì mặc định không ghi đè; dùng -f/--force để ghi đè.
- Dùng -t/--table để suy ra quy tắc từ bảng sở dữ liệu; để tạo cảnh thì thêm -s/--scenes (vd: crud).
EOF,
'tr' => <<<'EOF'
Doğrulama sınıf dosyası oluşturur (varsayılan: app/validation altında).
Önerilen:
php webman make:validator UserValidator
php webman make:validator admin/UserValidator
php webman make:validator UserValidator -p admin
php webman make:validator UserValidator -P plugin/admin/app/validation
Notlar:
- Varsayılan olarak app/validation altında oluşturulur (büyük/küçük harf mevcut dizine göre).
- -p/--plugin ile varsayılan olarak plugin/<plugin>/app/validation altında oluşturulur.
- -P/--path ile belirtilen göreli dizinde (proje köküne göre) oluşturulur.
- Dosya zaten varsa varsayılan olarak üzerine yazılmaz; üzerine yazmak için -f/--force kullanın.
- -t/--table ile veritabanı tablosundan kurallar çıkarılır; sahneleri de oluşturmak için -s/--scenes (örn. crud) belirtin.
EOF,
'id' => <<<'EOF'
Membuat berkas kelas validasi (baku: di bawah app/validation).
Disarankan:
php webman make:validator UserValidator
php webman make:validator admin/UserValidator
php webman make:validator UserValidator -p admin
php webman make:validator UserValidator -P plugin/admin/app/validation
Catatan:
- Secara baku dibuat di bawah app/validation (huruf besar/kecil mengikuti direktori yang ada).
- Dengan -p/--plugin secara baku dibuat di bawah plugin/<plugin>/app/validation.
- Dengan -P/--path dibuat di direktori relatif yang ditentukan (terhadap root proyek).
- Jika berkas sudah ada, secara baku tidak menimpa; gunakan -f/--force untuk menimpa.
- Dengan -t/--table aturan disimpulkan dari tabel database; untuk membuat adegan, berikan juga -s/--scenes (mis. crud).
EOF,
'th' => <<<'EOF'
สร้างไฟล์คลาสตรวจสอบความถูกต้อง (ค่าเริ่มต้นอยู่ใต้ app/validation)
แนะนำ:
php webman make:validator UserValidator
php webman make:validator admin/UserValidator
php webman make:validator UserValidator -p admin
php webman make:validator UserValidator -P plugin/admin/app/validation
หมายเหตุ:
- ค่าเริ่มต้นจะสร้างใต้ app/validation (ตัวพิมพ์ตามโฟลเดอร์ที่มีอยู่)
- ใช้ -p/--plugin จะสร้างใต้ plugin/<plugin>/app/validation โดยค่าเริ่มต้น
- ใช้ -P/--path จะสร้างในโฟลเดอร์สัมพัทธ์ที่กำหนด (เทียบกับรากโปรเจกต์)
- ถ้ามีไฟล์อยู่แล้วโดยค่าเริ่มต้นจะไม่เขียนทับ ใช้ -f/--force เพื่อเขียนทับ
- ใช้ -t/--table จะสรุปกฎจากตารางฐานข้อมูล เพื่อสร้างฉากให้ระบุ -s/--scenes ด้วย (เช่น crud)
EOF,
];
}
}
@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Webman\Validation\Command\ValidatorGenerator\Contracts;
interface ConnectionResolverInterface
{
/**
* @throws \RuntimeException When connection cannot be resolved.
*/
public function resolve(?string $connectionName = null): SchemaConnectionInterface;
}
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Webman\Validation\Command\ValidatorGenerator\Contracts;
use Webman\Validation\Command\ValidatorGenerator\DTO\TableDefinition;
interface RuleInferrerInterface
{
/**
* @param array{exclude_columns?: list<string>} $options
* @return array{rules: array<string, string>, attributes: array<string, string>, scenes?: array<string, list<string>>}
*/
public function infer(TableDefinition $table, array $options = []): array;
}
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Webman\Validation\Command\ValidatorGenerator\Contracts;
/**
* A minimal DB connection abstraction for schema introspection.
*
* This intentionally avoids binding to a specific ORM (Illuminate/ThinkOrm).
*/
interface SchemaConnectionInterface
{
public function driverName(): string;
public function databaseName(): ?string;
/**
* @param array<int, mixed> $bindings
* @return array<int, array<string, mixed>>
*/
public function select(string $sql, array $bindings = []): array;
}
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Webman\Validation\Command\ValidatorGenerator\Contracts;
use Webman\Validation\Command\ValidatorGenerator\DTO\TableDefinition;
interface SchemaIntrospectorInterface
{
/**
* @throws \RuntimeException When schema cannot be read.
*/
public function introspect(SchemaConnectionInterface $connection, string $table): TableDefinition;
}
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Webman\Validation\Command\ValidatorGenerator\DTO;
final class ColumnDefinition
{
/**
* @param list<string> $enumValues
*/
public function __construct(
public readonly string $name,
public readonly string $dataType,
public readonly string $columnType,
public readonly bool $nullable,
public readonly mixed $defaultValue,
public readonly ?int $characterMaximumLength,
public readonly ?int $numericPrecision,
public readonly ?int $numericScale,
public readonly bool $unsigned,
public readonly bool $autoIncrement,
public readonly array $enumValues,
public readonly string $comment,
) {
}
}
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Webman\Validation\Command\ValidatorGenerator\DTO;
final class TableDefinition
{
/**
* @param list<ColumnDefinition> $columns
* @param list<string> $primaryKeyColumns
*/
public function __construct(
public readonly string $table,
public readonly array $columns,
public readonly array $primaryKeyColumns = [],
) {
}
}
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Webman\Validation\Command\ValidatorGenerator\Illuminate;
use Illuminate\Database\ConnectionInterface;
use Webman\Validation\Command\ValidatorGenerator\Contracts\ConnectionResolverInterface;
use Webman\Validation\Command\ValidatorGenerator\Contracts\SchemaConnectionInterface;
final class IlluminateConnectionResolver implements ConnectionResolverInterface
{
public function resolve(?string $connectionName = null): SchemaConnectionInterface
{
if (!class_exists(\support\Db::class)) {
throw new \RuntimeException('Database support not found. Please install/enable webman/database plugin.');
}
$dbConfig = config('database', []);
if (!is_array($dbConfig)) {
throw new \RuntimeException('Invalid database config: config("database") must be an array.');
}
$connections = $dbConfig['connections'] ?? null;
if (!is_array($connections) || $connections === []) {
throw new \RuntimeException('Invalid database config: database.connections must be a non-empty array.');
}
$name = $connectionName;
if ($name === null || trim($name) === '') {
$default = $dbConfig['default'] ?? null;
if (!is_string($default) || trim($default) === '') {
throw new \RuntimeException('Database connection name not provided and database.default is not set.');
}
$name = trim($default);
}
$name = trim((string)$name);
$connKey = $name;
if (str_starts_with($name, 'plugin.')) {
// plugin.<plugin>.<connection>
$parts = explode('.', $name, 3);
$plugin = $parts[1] ?? '';
$conn = $parts[2] ?? '';
$plugin = is_string($plugin) ? trim($plugin) : '';
$conn = is_string($conn) ? trim($conn) : '';
if ($plugin === '' || $conn === '') {
throw new \RuntimeException("Invalid plugin connection name: {$name}");
}
$pluginDb = config("plugin.$plugin.database", []);
$pluginConnections = (is_array($pluginDb) ? ($pluginDb['connections'] ?? null) : null);
if (is_array($pluginConnections) && $pluginConnections !== []) {
if (!array_key_exists($conn, $pluginConnections)) {
$available = implode(', ', array_keys($pluginConnections));
throw new \RuntimeException("Database connection not found: {$name}. Available connections: {$available}");
}
$connKey = "plugin.$plugin.$conn";
} else {
// No plugin database connections: fallback to main project config.
$connKey = $conn;
if (!array_key_exists($connKey, $connections)) {
$available = implode(', ', array_keys($connections));
throw new \RuntimeException("Database connection not found: {$connKey}. Available connections: {$available}");
}
}
} else {
if (!array_key_exists($connKey, $connections)) {
$available = implode(', ', array_keys($connections));
throw new \RuntimeException("Database connection not found: {$connKey}. Available connections: {$available}");
}
}
/** @var ConnectionInterface $connection */
$connection = \support\Db::connection($connKey);
return new IlluminateSchemaConnection($connection);
}
}
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Webman\Validation\Command\ValidatorGenerator\Illuminate;
use Illuminate\Database\ConnectionInterface;
use Webman\Validation\Command\ValidatorGenerator\Contracts\SchemaConnectionInterface;
final class IlluminateSchemaConnection implements SchemaConnectionInterface
{
public function __construct(private readonly ConnectionInterface $connection)
{
}
public function driverName(): string
{
return (string)$this->connection->getDriverName();
}
public function databaseName(): ?string
{
$name = (string)$this->connection->getDatabaseName();
return $name !== '' ? $name : null;
}
public function select(string $sql, array $bindings = []): array
{
/** @var array<int, mixed> $rows */
$rows = $this->connection->select($sql, $bindings);
$result = [];
foreach ($rows as $row) {
if (is_array($row)) {
$result[] = $row;
continue;
}
if (is_object($row)) {
/** @var array<string, mixed> $arr */
$arr = get_object_vars($row);
$result[] = $arr;
continue;
}
$result[] = ['value' => $row];
}
return $result;
}
}
@@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
namespace Webman\Validation\Command\ValidatorGenerator\Illuminate;
use Webman\Validation\Command\ValidatorGenerator\Contracts\SchemaConnectionInterface;
use Webman\Validation\Command\ValidatorGenerator\Contracts\SchemaIntrospectorInterface;
use Webman\Validation\Command\ValidatorGenerator\DTO\ColumnDefinition;
use Webman\Validation\Command\ValidatorGenerator\DTO\TableDefinition;
final class MySqlInformationSchemaIntrospector implements SchemaIntrospectorInterface
{
public function introspect(SchemaConnectionInterface $connection, string $table): TableDefinition
{
$table = trim($table);
if ($table === '') {
throw new \InvalidArgumentException('Table name cannot be empty.');
}
$database = $connection->databaseName();
if (!$database) {
throw new \RuntimeException('Database name is empty for current connection.');
}
$rows = $connection->select(
"SELECT
COLUMN_NAME AS column_name,
DATA_TYPE AS data_type,
COLUMN_TYPE AS column_type,
IS_NULLABLE AS is_nullable,
COLUMN_DEFAULT AS column_default,
CHARACTER_MAXIMUM_LENGTH AS character_maximum_length,
NUMERIC_PRECISION AS numeric_precision,
NUMERIC_SCALE AS numeric_scale,
EXTRA AS extra,
COLUMN_COMMENT AS column_comment
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = ?
AND TABLE_NAME = ?
ORDER BY ORDINAL_POSITION",
[$database, $table]
);
if ($rows === []) {
throw new \RuntimeException("Table not found or has no columns: {$database}.{$table}");
}
$pkRows = $connection->select(
"SELECT
COLUMN_NAME AS column_name
FROM information_schema.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = ?
AND TABLE_NAME = ?
AND CONSTRAINT_NAME = 'PRIMARY'
ORDER BY ORDINAL_POSITION",
[$database, $table]
);
$primaryKeyColumns = [];
foreach ($pkRows as $pkRow) {
$pkName = (string)($pkRow['column_name'] ?? '');
if ($pkName !== '') {
$primaryKeyColumns[] = $pkName;
}
}
$columns = [];
foreach ($rows as $row) {
$name = (string)($row['column_name'] ?? '');
if ($name === '') {
continue;
}
$dataType = strtolower((string)($row['data_type'] ?? ''));
$columnType = strtolower((string)($row['column_type'] ?? $dataType));
$nullable = strtoupper((string)($row['is_nullable'] ?? 'NO')) === 'YES';
$defaultValue = $row['column_default'] ?? null;
$charLen = $row['character_maximum_length'] ?? null;
$characterMaximumLength = $charLen === null ? null : (int)$charLen;
$precision = $row['numeric_precision'] ?? null;
$numericPrecision = $precision === null ? null : (int)$precision;
$scale = $row['numeric_scale'] ?? null;
$numericScale = $scale === null ? null : (int)$scale;
$extra = strtolower((string)($row['extra'] ?? ''));
$autoIncrement = str_contains($extra, 'auto_increment');
$unsigned = str_contains($columnType, 'unsigned');
$comment = (string)($row['column_comment'] ?? '');
$enumValues = $this->parseEnumValues($dataType, $columnType);
$columns[] = new ColumnDefinition(
name: $name,
dataType: $dataType,
columnType: $columnType,
nullable: $nullable,
defaultValue: $defaultValue,
characterMaximumLength: $characterMaximumLength,
numericPrecision: $numericPrecision,
numericScale: $numericScale,
unsigned: $unsigned,
autoIncrement: $autoIncrement,
enumValues: $enumValues,
comment: $comment,
);
}
return new TableDefinition($table, $columns, $primaryKeyColumns);
}
/**
* @return list<string>
*/
private function parseEnumValues(string $dataType, string $columnType): array
{
if ($dataType !== 'enum' && !str_starts_with($columnType, 'enum(')) {
return [];
}
// column_type example: enum('a','b','c')
if (!preg_match('/^enum\((.*)\)$/i', $columnType, $m)) {
return [];
}
$inner = $m[1];
// Match MySQL quoted strings inside enum(...)
if (!preg_match_all("/'((?:\\\\'|[^'])*)'/", $inner, $matches)) {
return [];
}
$values = [];
foreach ($matches[1] as $v) {
$values[] = str_replace("\\'", "'", (string)$v);
}
return $values;
}
}
@@ -0,0 +1,186 @@
<?php
declare(strict_types=1);
namespace Webman\Validation\Command\ValidatorGenerator\Illuminate;
use Webman\Validation\Command\ValidatorGenerator\Contracts\SchemaConnectionInterface;
use Webman\Validation\Command\ValidatorGenerator\Contracts\SchemaIntrospectorInterface;
use Webman\Validation\Command\ValidatorGenerator\DTO\ColumnDefinition;
use Webman\Validation\Command\ValidatorGenerator\DTO\TableDefinition;
final class PostgresInformationSchemaIntrospector implements SchemaIntrospectorInterface
{
public function introspect(SchemaConnectionInterface $connection, string $table): TableDefinition
{
$table = trim($table);
if ($table === '') {
throw new \InvalidArgumentException('Table name cannot be empty.');
}
[$schema, $tableName] = $this->splitSchemaTable($table);
$rows = $connection->select(
"SELECT
c.column_name AS column_name,
c.data_type AS data_type,
c.udt_name AS udt_name,
c.is_nullable AS is_nullable,
c.column_default AS column_default,
c.character_maximum_length AS character_maximum_length,
c.numeric_precision AS numeric_precision,
c.numeric_scale AS numeric_scale
FROM information_schema.columns c
WHERE c.table_schema = ?
AND c.table_name = ?
ORDER BY c.ordinal_position",
[$schema, $tableName]
);
if ($rows === []) {
throw new \RuntimeException("Table not found or has no columns: {$schema}.{$tableName}");
}
$pkRows = $connection->select(
"SELECT kcu.column_name AS column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
AND tc.table_schema = kcu.table_schema
AND tc.table_name = kcu.table_name
WHERE tc.constraint_type = 'PRIMARY KEY'
AND tc.table_schema = ?
AND tc.table_name = ?
ORDER BY kcu.ordinal_position",
[$schema, $tableName]
);
$primaryKeyColumns = [];
foreach ($pkRows as $pkRow) {
$pkName = (string)($pkRow['column_name'] ?? '');
if ($pkName !== '') {
$primaryKeyColumns[] = $pkName;
}
}
// Fetch column comments (best-effort).
$commentRows = $connection->select(
"SELECT
a.attname AS column_name,
COALESCE(col_description(a.attrelid, a.attnum), '') AS column_comment
FROM pg_attribute a
JOIN pg_class t ON t.oid = a.attrelid
JOIN pg_namespace n ON n.oid = t.relnamespace
WHERE n.nspname = ?
AND t.relname = ?
AND a.attnum > 0
AND NOT a.attisdropped",
[$schema, $tableName]
);
$commentsByColumn = [];
foreach ($commentRows as $cr) {
$cn = (string)($cr['column_name'] ?? '');
if ($cn !== '') {
$commentsByColumn[$cn] = (string)($cr['column_comment'] ?? '');
}
}
// Fetch enum values (best-effort) for user-defined enum types.
$enumValuesByType = $this->loadEnumValuesByType($connection);
$columns = [];
foreach ($rows as $row) {
$name = (string)($row['column_name'] ?? '');
if ($name === '') {
continue;
}
$dataType = strtolower((string)($row['data_type'] ?? ''));
$udtName = strtolower((string)($row['udt_name'] ?? ''));
$nullable = strtoupper((string)($row['is_nullable'] ?? 'NO')) === 'YES';
$defaultValue = $row['column_default'] ?? null;
$charLen = $row['character_maximum_length'] ?? null;
$characterMaximumLength = $charLen === null ? null : (int)$charLen;
$precision = $row['numeric_precision'] ?? null;
$numericPrecision = $precision === null ? null : (int)$precision;
$scale = $row['numeric_scale'] ?? null;
$numericScale = $scale === null ? null : (int)$scale;
$autoIncrement = false;
if (is_string($defaultValue) && str_contains($defaultValue, 'nextval(')) {
$autoIncrement = true;
}
$comment = (string)($commentsByColumn[$name] ?? '');
$enumValues = [];
if ($dataType === 'user-defined' && isset($enumValuesByType[$udtName])) {
$enumValues = $enumValuesByType[$udtName];
$dataType = 'enum';
}
$columns[] = new ColumnDefinition(
name: $name,
dataType: $dataType,
columnType: $udtName !== '' ? $udtName : $dataType,
nullable: $nullable,
defaultValue: $defaultValue,
characterMaximumLength: $characterMaximumLength,
numericPrecision: $numericPrecision,
numericScale: $numericScale,
unsigned: false,
autoIncrement: $autoIncrement,
enumValues: $enumValues,
comment: $comment,
);
}
return new TableDefinition($tableName, $columns, $primaryKeyColumns);
}
/**
* @return array{0:string,1:string} [schema, table]
*/
private function splitSchemaTable(string $table): array
{
$parts = array_values(array_filter(explode('.', $table), static fn(string $p): bool => $p !== ''));
if (count($parts) === 1) {
return ['public', $parts[0]];
}
if (count($parts) === 2) {
return [$parts[0], $parts[1]];
}
throw new \InvalidArgumentException("Invalid table name for Postgres: {$table}");
}
/**
* @return array<string, list<string>> map udt_name => enum labels
*/
private function loadEnumValuesByType(SchemaConnectionInterface $connection): array
{
$rows = $connection->select(
"SELECT
t.typname AS type_name,
e.enumlabel AS enum_label
FROM pg_type t
JOIN pg_enum e ON e.enumtypid = t.oid
ORDER BY t.typname, e.enumsortorder"
);
$map = [];
foreach ($rows as $row) {
$type = strtolower((string)($row['type_name'] ?? ''));
$label = (string)($row['enum_label'] ?? '');
if ($type === '' || $label === '') {
continue;
}
$map[$type] ??= [];
$map[$type][] = $label;
}
return $map;
}
}
@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace Webman\Validation\Command\ValidatorGenerator\Illuminate;
use Webman\Validation\Command\ValidatorGenerator\Contracts\SchemaConnectionInterface;
use Webman\Validation\Command\ValidatorGenerator\Contracts\SchemaIntrospectorInterface;
use Webman\Validation\Command\ValidatorGenerator\DTO\ColumnDefinition;
use Webman\Validation\Command\ValidatorGenerator\DTO\TableDefinition;
final class SqlServerIntrospector implements SchemaIntrospectorInterface
{
public function introspect(SchemaConnectionInterface $connection, string $table): TableDefinition
{
$table = trim($table);
if ($table === '') {
throw new \InvalidArgumentException('Table name cannot be empty.');
}
[$schema, $tableName] = $this->splitSchemaTable($table);
$rows = $connection->select(
"SELECT
c.name AS column_name,
t.name AS data_type,
CASE
WHEN t.name IN ('nvarchar','nchar') THEN c.max_length / 2
ELSE c.max_length
END AS character_maximum_length,
c.precision AS numeric_precision,
c.scale AS numeric_scale,
c.is_nullable AS is_nullable,
dc.definition AS column_default,
c.is_identity AS is_identity,
ep.value AS column_comment
FROM sys.columns c
JOIN sys.tables tb ON tb.object_id = c.object_id
JOIN sys.schemas s ON s.schema_id = tb.schema_id
JOIN sys.types t ON t.user_type_id = c.user_type_id
LEFT JOIN sys.default_constraints dc ON dc.parent_object_id = c.object_id AND dc.parent_column_id = c.column_id
LEFT JOIN sys.extended_properties ep ON ep.major_id = c.object_id AND ep.minor_id = c.column_id AND ep.name = 'MS_Description'
WHERE s.name = ?
AND tb.name = ?
ORDER BY c.column_id",
[$schema, $tableName]
);
if ($rows === []) {
throw new \RuntimeException("Table not found or has no columns: {$schema}.{$tableName}");
}
$pkRows = $connection->select(
"SELECT c.name AS column_name
FROM sys.indexes i
JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id
JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id
JOIN sys.tables tb ON tb.object_id = i.object_id
JOIN sys.schemas s ON s.schema_id = tb.schema_id
WHERE i.is_primary_key = 1
AND s.name = ?
AND tb.name = ?
ORDER BY ic.key_ordinal",
[$schema, $tableName]
);
$primaryKeyColumns = [];
foreach ($pkRows as $pkRow) {
$pkName = (string)($pkRow['column_name'] ?? '');
if ($pkName !== '') {
$primaryKeyColumns[] = $pkName;
}
}
$columns = [];
foreach ($rows as $row) {
$name = (string)($row['column_name'] ?? '');
if ($name === '') {
continue;
}
$dataType = strtolower((string)($row['data_type'] ?? ''));
$nullable = (int)($row['is_nullable'] ?? 0) === 1;
$defaultValue = $row['column_default'] ?? null;
$charLen = $row['character_maximum_length'] ?? null;
$characterMaximumLength = $charLen === null ? null : (int)$charLen;
$precision = $row['numeric_precision'] ?? null;
$numericPrecision = $precision === null ? null : (int)$precision;
$scale = $row['numeric_scale'] ?? null;
$numericScale = $scale === null ? null : (int)$scale;
$autoIncrement = (int)($row['is_identity'] ?? 0) === 1;
$comment = is_string($row['column_comment'] ?? null) ? (string)$row['column_comment'] : '';
$columns[] = new ColumnDefinition(
name: $name,
dataType: $dataType,
columnType: $dataType,
nullable: $nullable,
defaultValue: $defaultValue,
characterMaximumLength: $characterMaximumLength,
numericPrecision: $numericPrecision,
numericScale: $numericScale,
unsigned: false,
autoIncrement: $autoIncrement,
enumValues: [],
comment: $comment,
);
}
return new TableDefinition($tableName, $columns, $primaryKeyColumns);
}
/**
* @return array{0:string,1:string} [schema, table]
*/
private function splitSchemaTable(string $table): array
{
$parts = array_values(array_filter(explode('.', $table), static fn(string $p): bool => $p !== ''));
if (count($parts) === 1) {
return ['dbo', $parts[0]];
}
if (count($parts) === 2) {
return [$parts[0], $parts[1]];
}
throw new \InvalidArgumentException("Invalid table name for SQL Server: {$table}");
}
}
@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace Webman\Validation\Command\ValidatorGenerator\Illuminate;
use Webman\Validation\Command\ValidatorGenerator\Contracts\SchemaConnectionInterface;
use Webman\Validation\Command\ValidatorGenerator\Contracts\SchemaIntrospectorInterface;
use Webman\Validation\Command\ValidatorGenerator\DTO\ColumnDefinition;
use Webman\Validation\Command\ValidatorGenerator\DTO\TableDefinition;
final class SqlitePragmaIntrospector implements SchemaIntrospectorInterface
{
public function introspect(SchemaConnectionInterface $connection, string $table): TableDefinition
{
$table = trim($table);
if ($table === '') {
throw new \InvalidArgumentException('Table name cannot be empty.');
}
if (!preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $table)) {
throw new \InvalidArgumentException("Invalid table name for SQLite: {$table}");
}
$rows = $connection->select("PRAGMA table_info('{$table}')");
if ($rows === []) {
throw new \RuntimeException("Table not found or has no columns: {$table}");
}
$primaryKeyColumns = [];
$columns = [];
foreach ($rows as $row) {
$name = (string)($row['name'] ?? '');
if ($name === '') {
continue;
}
$type = strtolower((string)($row['type'] ?? ''));
$notNull = (int)($row['notnull'] ?? 0) === 1;
$nullable = !$notNull;
$defaultValue = $row['dflt_value'] ?? null;
$pk = (int)($row['pk'] ?? 0);
if ($pk > 0) {
$primaryKeyColumns[] = $name;
}
[$dataType, $charLen, $precision, $scale] = $this->parseType($type);
$autoIncrement = false;
// Best-effort: INTEGER PRIMARY KEY behaves like auto-increment rowid.
if ($pk > 0 && $dataType === 'integer') {
$autoIncrement = true;
}
$columns[] = new ColumnDefinition(
name: $name,
dataType: $dataType,
columnType: $type !== '' ? $type : $dataType,
nullable: $nullable,
defaultValue: $defaultValue,
characterMaximumLength: $charLen,
numericPrecision: $precision,
numericScale: $scale,
unsigned: false,
autoIncrement: $autoIncrement,
enumValues: [],
comment: '',
);
}
return new TableDefinition($table, $columns, $primaryKeyColumns);
}
/**
* @return array{0:string,1:?int,2:?int,3:?int} [dataType, charLen, precision, scale]
*/
private function parseType(string $type): array
{
$type = strtolower(trim($type));
if ($type === '') {
return ['string', null, null, null];
}
if (str_contains($type, 'int')) {
return ['integer', null, null, null];
}
if (str_contains($type, 'char') || str_contains($type, 'clob') || str_contains($type, 'text')) {
$len = null;
if (preg_match('/\((\d+)\)/', $type, $m)) {
$len = (int)$m[1];
}
return ['varchar', $len, null, null];
}
if (str_contains($type, 'blob')) {
return ['string', null, null, null];
}
if (str_contains($type, 'real') || str_contains($type, 'floa') || str_contains($type, 'doub')) {
return ['double', null, null, null];
}
if (str_contains($type, 'dec') || str_contains($type, 'num')) {
$precision = null;
$scale = null;
if (preg_match('/\((\d+)\s*,\s*(\d+)\)/', $type, $m)) {
$precision = (int)$m[1];
$scale = (int)$m[2];
} elseif (preg_match('/\((\d+)\)/', $type, $m)) {
$precision = (int)$m[1];
}
return ['decimal', null, $precision, $scale];
}
if (str_contains($type, 'bool')) {
return ['boolean', null, null, null];
}
if (str_contains($type, 'date') || str_contains($type, 'time')) {
return ['datetime', null, null, null];
}
return ['string', null, null, null];
}
}
@@ -0,0 +1,174 @@
<?php
declare(strict_types=1);
namespace Webman\Validation\Command\ValidatorGenerator\Rules;
use Webman\Validation\Command\ValidatorGenerator\Contracts\RuleInferrerInterface;
use Webman\Validation\Command\ValidatorGenerator\DTO\ColumnDefinition;
use Webman\Validation\Command\ValidatorGenerator\DTO\TableDefinition;
final class DefaultRuleInferrer implements RuleInferrerInterface
{
public function infer(TableDefinition $table, array $options = []): array
{
$exclude = array_map('strtolower', $options['exclude_columns'] ?? []);
$excludeMap = array_fill_keys($exclude, true);
$withScenes = (bool)($options['with_scenes'] ?? false);
$rules = [];
$attributes = [];
foreach ($table->columns as $column) {
if (isset($excludeMap[strtolower($column->name)])) {
continue;
}
if ($column->autoIncrement && !$withScenes) {
// For create/update validators we generally don't validate auto-increment columns (e.g. id).
continue;
}
$ruleParts = $this->inferRuleParts($column);
if ($ruleParts === []) {
continue;
}
$rules[$column->name] = implode('|', $ruleParts);
$comment = trim($column->comment);
if ($comment !== '') {
$attributes[$column->name] = $comment;
}
}
$result = ['rules' => $rules, 'attributes' => $attributes];
if ($withScenes) {
$scenesType = strtolower(trim((string)($options['scenes'] ?? 'crud')));
if ($scenesType !== 'crud') {
throw new \InvalidArgumentException("Unsupported scenes type: {$scenesType}");
}
$result['scenes'] = $this->buildCrudScenes($table, $rules);
}
return $result;
}
/**
* @param array<string, string> $rules
* @return array<string, list<string>>
*/
private function buildCrudScenes(TableDefinition $table, array $rules): array
{
$ruleKeys = array_keys($rules);
$pk = array_values(array_intersect($table->primaryKeyColumns, $ruleKeys));
if ($pk === []) {
throw new \RuntimeException("Cannot generate CRUD scenes: primary key columns not found in rules for table {$table->table}");
}
$nonPk = array_values(array_diff($ruleKeys, $pk));
return [
'create' => $nonPk,
// Full update (PUT semantics) generally requires primary key + full payload.
'update' => array_values(array_merge($pk, $nonPk)),
'delete' => $pk,
'detail' => $pk,
];
}
/**
* @return list<string>
*/
private function inferRuleParts(ColumnDefinition $column): array
{
$parts = [];
if ($this->shouldBeRequired($column)) {
$parts[] = 'required';
} elseif ($column->nullable) {
$parts[] = 'nullable';
}
$typeParts = $this->inferTypeParts($column);
foreach ($typeParts as $p) {
$parts[] = $p;
}
return $parts;
}
private function shouldBeRequired(ColumnDefinition $column): bool
{
if ($column->nullable) {
return false;
}
// If default exists, allow omitting the field.
if ($column->defaultValue !== null) {
return false;
}
return true;
}
/**
* @return list<string>
*/
private function inferTypeParts(ColumnDefinition $column): array
{
$type = strtolower($column->dataType);
if ($column->enumValues !== []) {
// Values containing commas are not representable; keep best-effort.
return ['in:' . implode(',', $column->enumValues)];
}
return match ($type) {
'varchar', 'char' => $this->stringRules($column),
'text', 'tinytext', 'mediumtext', 'longtext' => ['string'],
'int', 'integer', 'tinyint', 'smallint', 'mediumint', 'bigint' => $this->integerRules($column),
'decimal', 'numeric', 'float', 'double' => $this->numericRules($column),
'date', 'datetime', 'timestamp', 'time' => ['date'],
'json' => ['json'],
'bool', 'boolean' => ['boolean'],
default => [],
};
}
/**
* @return list<string>
*/
private function stringRules(ColumnDefinition $column): array
{
$rules = ['string'];
if ($column->characterMaximumLength !== null && $column->characterMaximumLength > 0) {
$rules[] = 'max:' . $column->characterMaximumLength;
}
return $rules;
}
/**
* @return list<string>
*/
private function integerRules(ColumnDefinition $column): array
{
$rules = ['integer'];
if ($column->unsigned) {
$rules[] = 'min:0';
}
return $rules;
}
/**
* @return list<string>
*/
private function numericRules(ColumnDefinition $column): array
{
$rules = ['numeric'];
if ($column->unsigned) {
$rules[] = 'min:0';
}
return $rules;
}
}

Some files were not shown because too many files have changed in this diff Show More