暂存
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
build
|
||||
vendor
|
||||
.idea
|
||||
.vscode
|
||||
.phpunit*
|
||||
composer.lock
|
||||
Vendored
+1199
File diff suppressed because it is too large
Load Diff
Vendored
+38
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+1303
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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+3758
File diff suppressed because it is too large
Load Diff
+9
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Webman\Console;
|
||||
|
||||
final class OrmType
|
||||
{
|
||||
public const LARAVEL = 'laravel';
|
||||
public const THINKORM = 'thinkorm';
|
||||
}
|
||||
Vendored
+495
@@ -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
@@ -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();
|
||||
Vendored
+21
@@ -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.
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
# Webman database
|
||||
This is Webman's database library, which includes database client and connection pool.
|
||||
Vendored
+20
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
{
|
||||
|
||||
}
|
||||
Vendored
+21
@@ -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.
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
# Webman redis
|
||||
This is Webman's redis library, which includes redis client and connection pool.
|
||||
Vendored
+18
@@ -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
|
||||
}
|
||||
Vendored
+73
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
Vendored
+2148
File diff suppressed because it is too large
Load Diff
+2150
File diff suppressed because it is too large
Load Diff
+26
@@ -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": "*"
|
||||
}
|
||||
}
|
||||
Vendored
+10
@@ -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' => [],
|
||||
];
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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' => '使用的 ORM:auto|laravel|thinkorm(默认:auto)',
|
||||
'zh_TW' => '使用的 ORM:auto|laravel|thinkorm(預設:auto)',
|
||||
'en' => 'ORM to use: auto|laravel|thinkorm (default: auto)',
|
||||
'ja' => '使用する ORM:auto|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 já 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 có).
|
||||
- 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 cơ 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
Vendored
+13
@@ -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;
|
||||
}
|
||||
|
||||
Vendored
+16
@@ -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;
|
||||
}
|
||||
|
||||
Vendored
+23
@@ -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;
|
||||
}
|
||||
|
||||
Vendored
+15
@@ -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;
|
||||
}
|
||||
|
||||
+27
@@ -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,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
+19
@@ -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 = [],
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+78
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+49
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+141
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+186
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+131
@@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+120
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
+174
@@ -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
Reference in New Issue
Block a user