暂存
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webman\Validation\Command\ValidatorGenerator\Support;
|
||||
|
||||
final class ExcludedColumns
|
||||
{
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function defaultForIlluminate(): array
|
||||
{
|
||||
return ['created_at', 'updated_at', 'deleted_at'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function defaultForThinkOrm(): array
|
||||
{
|
||||
// ThinkORM (ThinkPHP) commonly uses *_time fields for timestamps/soft delete.
|
||||
return ['create_time', 'update_time', 'delete_time'];
|
||||
}
|
||||
}
|
||||
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webman\Validation\Command\ValidatorGenerator\Support;
|
||||
|
||||
final class OrmDetector
|
||||
{
|
||||
public const ORM_AUTO = 'auto';
|
||||
public const ORM_LARAVEL = 'laravel';
|
||||
public const ORM_THINKORM = 'thinkorm';
|
||||
|
||||
/**
|
||||
* @return array{laravel: bool, thinkorm: bool}
|
||||
*/
|
||||
public function availability(): array
|
||||
{
|
||||
return [
|
||||
'laravel' => $this->isIlluminateAvailable(),
|
||||
'thinkorm' => $this->isThinkOrmAvailable(),
|
||||
];
|
||||
}
|
||||
|
||||
public function resolve(string $requestedOrm): string
|
||||
{
|
||||
$requestedOrm = strtolower(trim($requestedOrm));
|
||||
if ($requestedOrm === '' || $requestedOrm === self::ORM_AUTO) {
|
||||
$availability = $this->availability();
|
||||
if ($availability['laravel']) {
|
||||
return self::ORM_LARAVEL;
|
||||
}
|
||||
if ($availability['thinkorm']) {
|
||||
return self::ORM_THINKORM;
|
||||
}
|
||||
throw new \RuntimeException('No ORM available. Please install/configure illuminate/database or think-orm.');
|
||||
}
|
||||
|
||||
// Backward compatible alias: `illuminate` => `laravel`
|
||||
if ($requestedOrm === 'illuminate') {
|
||||
$requestedOrm = self::ORM_LARAVEL;
|
||||
}
|
||||
|
||||
if ($requestedOrm === self::ORM_LARAVEL) {
|
||||
if (!$this->isIlluminateAvailable()) {
|
||||
throw new \RuntimeException('Requested orm=laravel but illuminate/database is not available.');
|
||||
}
|
||||
return self::ORM_LARAVEL;
|
||||
}
|
||||
|
||||
if ($requestedOrm === self::ORM_THINKORM) {
|
||||
if (!$this->isThinkOrmAvailable()) {
|
||||
throw new \RuntimeException('Requested orm=thinkorm but think-orm is not available.');
|
||||
}
|
||||
return self::ORM_THINKORM;
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException("Unsupported orm value: {$requestedOrm}");
|
||||
}
|
||||
|
||||
private function isIlluminateAvailable(): bool
|
||||
{
|
||||
if (!class_exists(\support\Db::class)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$database = config('database');
|
||||
if (!is_array($database)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Follow webman make:model behavior: plugin.* defaults are not treated as local database config.
|
||||
$default = $database['default'] ?? null;
|
||||
if (is_string($default) && str_starts_with($default, 'plugin.')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$connections = $database['connections'] ?? null;
|
||||
return is_array($connections) && $connections !== [];
|
||||
}
|
||||
|
||||
private function isThinkOrmAvailable(): bool
|
||||
{
|
||||
$thinkorm = config('think-orm') ?: config('thinkorm');
|
||||
if (!is_array($thinkorm)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$default = $thinkorm['default'] ?? null;
|
||||
if (is_string($default) && str_starts_with($default, 'plugin.')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$connections = $thinkorm['connections'] ?? null;
|
||||
if (!is_array($connections) || $connections === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Think-orm v2 uses support\think\Db; v1 uses think\facade\Db.
|
||||
$hasRuntime = class_exists(\support\think\Db::class) || class_exists(\think\facade\Db::class);
|
||||
if (!$hasRuntime) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webman\Validation\Command\ValidatorGenerator\Support;
|
||||
|
||||
final class PhpArrayExporter
|
||||
{
|
||||
public static function export(array $value, int $indentLevel = 1): string
|
||||
{
|
||||
return self::exportArray($value, $indentLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export an array for use as a class property initializer.
|
||||
*
|
||||
* Expected output style:
|
||||
* - Empty: []
|
||||
* - Non-empty:
|
||||
* [
|
||||
* 'k' => 'v',
|
||||
* ]
|
||||
*/
|
||||
public static function exportForProperty(array $value, int $propertyIndentLevel = 1): string
|
||||
{
|
||||
if ($value === []) {
|
||||
return '[]';
|
||||
}
|
||||
|
||||
$propertyIndent = str_repeat(' ', $propertyIndentLevel);
|
||||
$childIndent = str_repeat(' ', $propertyIndentLevel + 1);
|
||||
|
||||
$lines = ['['];
|
||||
foreach ($value as $k => $v) {
|
||||
$key = is_int($k) ? null : self::exportScalar($k);
|
||||
|
||||
if (is_array($v)) {
|
||||
$exported = self::exportArrayValue($v, $propertyIndentLevel + 1);
|
||||
} else {
|
||||
$exported = self::exportScalar($v);
|
||||
}
|
||||
|
||||
if ($key === null) {
|
||||
$lines[] = "{$childIndent}{$exported},";
|
||||
} else {
|
||||
$lines[] = "{$childIndent}{$key} => {$exported},";
|
||||
}
|
||||
}
|
||||
$lines[] = "{$propertyIndent}]";
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
private static function exportArray(array $value, int $indentLevel): string
|
||||
{
|
||||
if ($value === []) {
|
||||
return '[]';
|
||||
}
|
||||
|
||||
$indent = str_repeat(' ', $indentLevel);
|
||||
$childIndent = str_repeat(' ', $indentLevel + 1);
|
||||
|
||||
$lines = ["{$indent}["];
|
||||
foreach ($value as $k => $v) {
|
||||
$key = is_int($k) ? null : self::exportScalar($k);
|
||||
|
||||
if (is_array($v)) {
|
||||
$exported = self::exportArray($v, $indentLevel + 1);
|
||||
} else {
|
||||
$exported = self::exportScalar($v);
|
||||
}
|
||||
|
||||
if ($key === null) {
|
||||
$lines[] = "{$childIndent}{$exported},";
|
||||
} else {
|
||||
$lines[] = "{$childIndent}{$key} => {$exported},";
|
||||
}
|
||||
}
|
||||
$lines[] = "{$indent}]";
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export an array as a value after `=> `, i.e. first line has no leading indent.
|
||||
*/
|
||||
private static function exportArrayValue(array $value, int $closingIndentLevel): string
|
||||
{
|
||||
if ($value === []) {
|
||||
return '[]';
|
||||
}
|
||||
|
||||
$closingIndent = str_repeat(' ', $closingIndentLevel);
|
||||
$childIndent = str_repeat(' ', $closingIndentLevel + 1);
|
||||
|
||||
$lines = ['['];
|
||||
foreach ($value as $k => $v) {
|
||||
$key = is_int($k) ? null : self::exportScalar($k);
|
||||
|
||||
if (is_array($v)) {
|
||||
$exported = self::exportArrayValue($v, $closingIndentLevel + 1);
|
||||
} else {
|
||||
$exported = self::exportScalar($v);
|
||||
}
|
||||
|
||||
if ($key === null) {
|
||||
$lines[] = "{$childIndent}{$exported},";
|
||||
} else {
|
||||
$lines[] = "{$childIndent}{$key} => {$exported},";
|
||||
}
|
||||
}
|
||||
$lines[] = "{$closingIndent}]";
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
private static function exportScalar(mixed $value): string
|
||||
{
|
||||
if ($value === null) {
|
||||
return 'null';
|
||||
}
|
||||
if (is_bool($value)) {
|
||||
return $value ? 'true' : 'false';
|
||||
}
|
||||
if (is_int($value) || is_float($value)) {
|
||||
return (string)$value;
|
||||
}
|
||||
if (is_string($value)) {
|
||||
return "'" . str_replace(["\\", "'"], ["\\\\", "\\'"], $value) . "'";
|
||||
}
|
||||
|
||||
// Fallback to string casting for unexpected scalars.
|
||||
return "'" . str_replace(["\\", "'"], ["\\\\", "\\'"], (string)$value) . "'";
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webman\Validation\Command\ValidatorGenerator\Support;
|
||||
|
||||
use Webman\Validation\Command\ValidatorGenerator\Contracts\SchemaIntrospectorInterface;
|
||||
use Webman\Validation\Command\ValidatorGenerator\Illuminate\MySqlInformationSchemaIntrospector;
|
||||
use Webman\Validation\Command\ValidatorGenerator\Illuminate\PostgresInformationSchemaIntrospector;
|
||||
use Webman\Validation\Command\ValidatorGenerator\Illuminate\SqlitePragmaIntrospector;
|
||||
use Webman\Validation\Command\ValidatorGenerator\Illuminate\SqlServerIntrospector;
|
||||
|
||||
final class SchemaIntrospectorFactory
|
||||
{
|
||||
public function createForDriver(string $driver): SchemaIntrospectorInterface
|
||||
{
|
||||
$driver = strtolower(trim($driver));
|
||||
|
||||
return match ($driver) {
|
||||
'mysql', 'mariadb' => new MySqlInformationSchemaIntrospector(),
|
||||
'pgsql', 'postgres', 'postgresql' => new PostgresInformationSchemaIntrospector(),
|
||||
'sqlite' => new SqlitePragmaIntrospector(),
|
||||
'sqlsrv' => new SqlServerIntrospector(),
|
||||
default => throw new \RuntimeException("Unsupported database driver: {$driver}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webman\Validation\Command\ValidatorGenerator\Support;
|
||||
|
||||
final class ValidatorClassRenderer
|
||||
{
|
||||
/**
|
||||
* @param array<string, string> $rules
|
||||
* @param array<string, string> $messages
|
||||
* @param array<string, string> $attributes
|
||||
* @param array<string, list<string>> $scenes
|
||||
*/
|
||||
public function render(
|
||||
string $namespace,
|
||||
string $class,
|
||||
array $rules,
|
||||
array $messages = [],
|
||||
array $attributes = [],
|
||||
array $scenes = [],
|
||||
): string {
|
||||
$rulesCode = PhpArrayExporter::exportForProperty($rules, 1);
|
||||
$messagesCode = PhpArrayExporter::exportForProperty($messages, 1);
|
||||
$attributesCode = PhpArrayExporter::exportForProperty($attributes, 1);
|
||||
$scenesCode = PhpArrayExporter::exportForProperty($scenes, 1);
|
||||
|
||||
return <<<PHP
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace {$namespace};
|
||||
|
||||
use support\\validation\\Validator;
|
||||
|
||||
class {$class} extends Validator
|
||||
{
|
||||
protected array \$rules = {$rulesCode};
|
||||
|
||||
protected array \$messages = {$messagesCode};
|
||||
|
||||
protected array \$attributes = {$attributesCode};
|
||||
|
||||
protected array \$scenes = {$scenesCode};
|
||||
}
|
||||
|
||||
PHP;
|
||||
}
|
||||
}
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webman\Validation\Command\ValidatorGenerator\Support;
|
||||
|
||||
final class ValidatorFileWriter
|
||||
{
|
||||
public function write(string $file, string $content): void
|
||||
{
|
||||
$dir = \dirname($file);
|
||||
if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) {
|
||||
throw new \RuntimeException("Failed to create directory: {$dir}");
|
||||
}
|
||||
|
||||
if (file_put_contents($file, $content) === false) {
|
||||
throw new \RuntimeException("Failed to write file: {$file}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+102
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webman\Validation\Command\ValidatorGenerator\ThinkOrm;
|
||||
|
||||
use Webman\Validation\Command\ValidatorGenerator\Contracts\ConnectionResolverInterface;
|
||||
use Webman\Validation\Command\ValidatorGenerator\Contracts\SchemaConnectionInterface;
|
||||
|
||||
final class ThinkOrmConnectionResolver implements ConnectionResolverInterface
|
||||
{
|
||||
public function resolve(?string $connectionName = null): SchemaConnectionInterface
|
||||
{
|
||||
$thinkorm = config('think-orm');
|
||||
if (!is_array($thinkorm) || $thinkorm === []) {
|
||||
$alt = config('thinkorm');
|
||||
$thinkorm = is_array($alt) ? $alt : null;
|
||||
}
|
||||
if (!is_array($thinkorm)) {
|
||||
throw new \RuntimeException('Think-orm config not found: config("think-orm") or config("thinkorm").');
|
||||
}
|
||||
|
||||
$mainConnections = $thinkorm['connections'] ?? null;
|
||||
if (!is_array($mainConnections) || $mainConnections === []) {
|
||||
throw new \RuntimeException('Invalid think-orm config: connections must be a non-empty array.');
|
||||
}
|
||||
|
||||
$name = $connectionName;
|
||||
if ($name === null || trim($name) === '') {
|
||||
$default = $thinkorm['default'] ?? null;
|
||||
if (!is_string($default) || trim($default) === '') {
|
||||
throw new \RuntimeException('Think-orm connection name not provided and think-orm.default is not set.');
|
||||
}
|
||||
$name = trim($default);
|
||||
}
|
||||
|
||||
$name = trim((string)$name);
|
||||
$connKey = $name;
|
||||
$connections = $mainConnections;
|
||||
|
||||
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}");
|
||||
}
|
||||
|
||||
$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 !== []) {
|
||||
$connections = $pluginConnections;
|
||||
$connKey = "plugin.$plugin.$conn";
|
||||
$name = $conn;
|
||||
} else {
|
||||
// No plugin think-orm connections: fallback to main project config.
|
||||
$connections = $mainConnections;
|
||||
$connKey = $conn;
|
||||
$name = $conn;
|
||||
}
|
||||
}
|
||||
|
||||
if (!array_key_exists($name, $connections)) {
|
||||
$available = implode(', ', array_keys($connections));
|
||||
throw new \RuntimeException("Think-orm connection not found: {$name}. Available connections: {$available}");
|
||||
}
|
||||
|
||||
/** @var array<string, mixed> $cfg */
|
||||
$cfg = is_array($connections[$name]) ? $connections[$name] : [];
|
||||
$driver = (string)($cfg['type'] ?? 'mysql');
|
||||
$database = isset($cfg['database']) ? (string)$cfg['database'] : null;
|
||||
|
||||
$connection = $this->connect($connKey);
|
||||
return new ThinkOrmSchemaConnection($connection, strtolower($driver), $database);
|
||||
}
|
||||
|
||||
private function connect(string $name): object
|
||||
{
|
||||
// Think-orm v2
|
||||
if (class_exists(\support\think\Db::class)) {
|
||||
/** @var object $conn */
|
||||
$conn = \support\think\Db::connect($name);
|
||||
return $conn;
|
||||
}
|
||||
|
||||
// Think-orm v1
|
||||
if (class_exists(\think\facade\Db::class)) {
|
||||
/** @var object $conn */
|
||||
$conn = \think\facade\Db::connect($name);
|
||||
return $conn;
|
||||
}
|
||||
|
||||
throw new \RuntimeException('Think-orm is not installed. Missing support\\think\\Db or think\\facade\\Db.');
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+61
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webman\Validation\Command\ValidatorGenerator\ThinkOrm;
|
||||
|
||||
use Webman\Validation\Command\ValidatorGenerator\Contracts\SchemaConnectionInterface;
|
||||
|
||||
final class ThinkOrmSchemaConnection implements SchemaConnectionInterface
|
||||
{
|
||||
/**
|
||||
* @param object $connection Think-orm connection instance.
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly object $connection,
|
||||
private readonly string $driver,
|
||||
private readonly ?string $database,
|
||||
) {
|
||||
}
|
||||
|
||||
public function driverName(): string
|
||||
{
|
||||
return $this->driver;
|
||||
}
|
||||
|
||||
public function databaseName(): ?string
|
||||
{
|
||||
return $this->database;
|
||||
}
|
||||
|
||||
public function select(string $sql, array $bindings = []): array
|
||||
{
|
||||
// Think-orm uses `query` for SELECT statements.
|
||||
if (!method_exists($this->connection, 'query')) {
|
||||
throw new \RuntimeException('Think-orm connection does not support query().');
|
||||
}
|
||||
|
||||
/** @var mixed $rows */
|
||||
$rows = $this->connection->query($sql, $bindings);
|
||||
if (!is_array($rows)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($rows as $row) {
|
||||
if (is_array($row)) {
|
||||
$result[] = $row;
|
||||
continue;
|
||||
}
|
||||
if (is_object($row)) {
|
||||
/** @var array<string, mixed> $arr */
|
||||
$arr = get_object_vars($row);
|
||||
$result[] = $arr;
|
||||
continue;
|
||||
}
|
||||
$result[] = ['value' => $row];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webman\Validation\Exception;
|
||||
|
||||
use Webman\Exception\BusinessException;
|
||||
|
||||
class ValidationException extends BusinessException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webman\Validation\Factory;
|
||||
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Translation\FileLoader;
|
||||
use Illuminate\Translation\Translator;
|
||||
use Illuminate\Validation\Factory;
|
||||
use Illuminate\Validation\DatabasePresenceVerifier;
|
||||
use Illuminate\Validation\DatabasePresenceVerifierInterface;
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
final class ValidationFactory
|
||||
{
|
||||
private static ?Factory $factory = null;
|
||||
private static bool $presenceVerifierBound = false;
|
||||
|
||||
public static function getFactory(): Factory
|
||||
{
|
||||
if (self::$factory === null) {
|
||||
self::$factory = self::createFactory();
|
||||
}
|
||||
|
||||
if (!self::$presenceVerifierBound) {
|
||||
self::bindPresenceVerifier(self::$factory);
|
||||
}
|
||||
|
||||
return self::$factory;
|
||||
}
|
||||
|
||||
private static function createFactory(): Factory
|
||||
{
|
||||
$loader = self::createLoader();
|
||||
$translator = new Translator($loader, self::getLocale());
|
||||
|
||||
$fallback = self::getFallbackLocale();
|
||||
if ($fallback !== '') {
|
||||
$translator->setFallback($fallback);
|
||||
}
|
||||
|
||||
$factory = new Factory($translator);
|
||||
self::bindFacadeRoot($factory);
|
||||
self::bindPresenceVerifier($factory);
|
||||
return $factory;
|
||||
}
|
||||
|
||||
private static function bindFacadeRoot(Factory $factory): void
|
||||
{
|
||||
if (Facade::getFacadeApplication() !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$container = new Container();
|
||||
$container->instance('validator', $factory);
|
||||
Facade::setFacadeApplication($container);
|
||||
}
|
||||
|
||||
private static function bindPresenceVerifier(Factory $factory): void
|
||||
{
|
||||
try {
|
||||
if ($factory->getPresenceVerifier() instanceof DatabasePresenceVerifierInterface) {
|
||||
self::$presenceVerifierBound = true;
|
||||
return;
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// Verifier not yet set; continue to try binding.
|
||||
}
|
||||
|
||||
if (!class_exists(DatabasePresenceVerifier::class) || !class_exists(Model::class)) {
|
||||
self::$presenceVerifierBound = true;
|
||||
return;
|
||||
}
|
||||
|
||||
$resolver = Model::getConnectionResolver();
|
||||
if ($resolver === null && class_exists('support\\Model')) {
|
||||
$resolver = Model::getConnectionResolver();
|
||||
}
|
||||
if ($resolver === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$factory->setPresenceVerifier(new DatabasePresenceVerifier($resolver));
|
||||
self::$presenceVerifierBound = true;
|
||||
}
|
||||
|
||||
private static function createLoader(): FileLoader
|
||||
{
|
||||
$filesystem = new Filesystem();
|
||||
|
||||
$packagePath = self::getPackageTranslationsPath();
|
||||
$illuminatePath = self::getIlluminateTranslationsPath();
|
||||
$projectPaths = self::getProjectTranslationsPaths();
|
||||
|
||||
$orderedPaths = array_values(array_filter([
|
||||
$packagePath,
|
||||
$illuminatePath,
|
||||
...$projectPaths,
|
||||
], fn ($path) => $path !== ''));
|
||||
|
||||
$basePath = $orderedPaths[0] ?? '';
|
||||
$loader = new FileLoader($filesystem, $basePath);
|
||||
|
||||
foreach ($orderedPaths as $path) {
|
||||
if ($path !== $basePath) {
|
||||
$loader->addPath($path);
|
||||
}
|
||||
}
|
||||
|
||||
return $loader;
|
||||
}
|
||||
|
||||
private static function getLocale(): string
|
||||
{
|
||||
if (self::canUseLocaleFunction() && function_exists('locale')) {
|
||||
$locale = locale();
|
||||
if (is_string($locale) && $locale !== '') {
|
||||
return $locale;
|
||||
}
|
||||
}
|
||||
|
||||
$locale = config('translation.locale', 'en');
|
||||
return is_string($locale) && $locale !== '' ? $locale : 'en';
|
||||
}
|
||||
|
||||
private static function getFallbackLocale(): string
|
||||
{
|
||||
$fallback = config('translation.fallback_locale', []);
|
||||
if (is_array($fallback)) {
|
||||
$fallback = $fallback[0] ?? '';
|
||||
}
|
||||
return is_string($fallback) ? $fallback : '';
|
||||
}
|
||||
|
||||
private static function getProjectTranslationsPaths(): array
|
||||
{
|
||||
$path = config('translation.path', '');
|
||||
$paths = is_array($path) ? $path : [$path];
|
||||
|
||||
$result = [];
|
||||
foreach ($paths as $item) {
|
||||
if (is_string($item) && $item !== '' && is_dir($item)) {
|
||||
$result[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function canUseLocaleFunction(): bool
|
||||
{
|
||||
$path = config('translation.path', '');
|
||||
return is_string($path) && $path !== '' && is_dir($path);
|
||||
}
|
||||
|
||||
private static function getPackageTranslationsPath(): string
|
||||
{
|
||||
$path = dirname(__DIR__, 2) . '/resources/lang';
|
||||
return is_dir($path) ? $path : '';
|
||||
}
|
||||
|
||||
private static function getIlluminateTranslationsPath(): string
|
||||
{
|
||||
$path = dirname(__DIR__, 2) . '/vendor/illuminate/translation/lang';
|
||||
return is_dir($path) ? $path : '';
|
||||
}
|
||||
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
namespace Webman\Validation;
|
||||
|
||||
class Install
|
||||
{
|
||||
const WEBMAN_PLUGIN = true;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected static array $pathRelation = [
|
||||
'config/plugin/webman/validation' => 'config/plugin/webman/validation',
|
||||
];
|
||||
|
||||
/**
|
||||
* 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
|
||||
";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
";
|
||||
if (is_file($path) || is_link($path)) {
|
||||
unlink($path);
|
||||
continue;
|
||||
}
|
||||
remove_dir($path);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+417
@@ -0,0 +1,417 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webman\Validation;
|
||||
|
||||
use Closure;
|
||||
use InvalidArgumentException;
|
||||
use ReflectionFunction;
|
||||
use ReflectionFunctionAbstract;
|
||||
use ReflectionMethod;
|
||||
use ReflectionNamedType;
|
||||
use ReflectionParameter;
|
||||
use ReflectionUnionType;
|
||||
use Webman\Http\Request;
|
||||
use Webman\Validation\Annotation\Param;
|
||||
use Webman\Validation\Annotation\Validate;
|
||||
|
||||
final class Middleware
|
||||
{
|
||||
private static array $metadataCache = [];
|
||||
|
||||
public function process(Request $request, callable $handler)
|
||||
{
|
||||
$metadata = $this->resolveMetadata($request);
|
||||
if ($metadata === null || !$metadata['has']) {
|
||||
return $handler($request);
|
||||
}
|
||||
|
||||
$defaultData = $this->getRequestData($request);
|
||||
|
||||
$this->handleMethodValidation($request, $metadata['methods'], $defaultData);
|
||||
$this->handleParamValidation($request, $metadata['params'], $defaultData);
|
||||
|
||||
return $handler($request);
|
||||
}
|
||||
|
||||
private function resolveMetadata(Request $request): ?array
|
||||
{
|
||||
$controller = $request->controller ?: '';
|
||||
$action = $request->action ?: '';
|
||||
|
||||
if ($controller !== '' && $action !== '' && class_exists($controller)) {
|
||||
return $this->getMethodMetadata($controller, $action);
|
||||
}
|
||||
|
||||
return $this->getCallableMetadata($request);
|
||||
}
|
||||
|
||||
private function handleMethodValidation(Request $request, array $methods, array $defaultData): void
|
||||
{
|
||||
if ($methods === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($methods as $config) {
|
||||
$data = $this->resolveRequestData($request, $config->in, $defaultData);
|
||||
$this->validateMethod($config, $data);
|
||||
}
|
||||
}
|
||||
|
||||
private function handleParamValidation(Request $request, array $params, array $defaultData): void
|
||||
{
|
||||
if ($params === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$allData = [];
|
||||
$allRules = [];
|
||||
$allMessages = [];
|
||||
$allAttributes = [];
|
||||
|
||||
foreach ($params as $item) {
|
||||
$name = $item['name'];
|
||||
/** @var \Webman\Validation\Annotation\Param $config */
|
||||
$config = $item['config'];
|
||||
|
||||
$dataForParam = $this->resolveRequestData($request, $config->in, $defaultData);
|
||||
$value = $dataForParam[$name] ?? null;
|
||||
if ($value === null && $item['hasDefault']) {
|
||||
$value = $item['default'];
|
||||
}
|
||||
|
||||
$allData[$name] = $value;
|
||||
$allRules[$name] = $config->rules;
|
||||
|
||||
// 处理 messages,确保 key 带有字段前缀,避免冲突
|
||||
foreach ($config->messages as $key => $message) {
|
||||
if (!str_contains($key, '.')) {
|
||||
// 没有点号的 key 自动添加字段名前缀
|
||||
$key = $name . '.' . $key;
|
||||
}
|
||||
$allMessages[$key] = $message;
|
||||
}
|
||||
|
||||
if ($config->attribute !== '') {
|
||||
$allAttributes[$name] = $config->attribute;
|
||||
}
|
||||
}
|
||||
|
||||
Validator::make($allData, $allRules, $allMessages, $allAttributes)->validate();
|
||||
}
|
||||
|
||||
private function validateMethod(Validate $config, array $data): void
|
||||
{
|
||||
if ($config->validator !== null) {
|
||||
if ($config->rules !== []) {
|
||||
throw new InvalidArgumentException('Validate cannot set both validator and rules.');
|
||||
}
|
||||
if (!class_exists($config->validator)) {
|
||||
throw new InvalidArgumentException("Validator class not found: {$config->validator}");
|
||||
}
|
||||
if (!is_subclass_of($config->validator, \Webman\Validation\Validator::class)) {
|
||||
throw new InvalidArgumentException("Validator must extend Webman\\Validation\\Validator (or support\\validation\\Validator): {$config->validator}");
|
||||
}
|
||||
|
||||
$validator = $config->validator::make($data);
|
||||
if ($config->scene !== null) {
|
||||
$validator = $validator->withScene($config->scene);
|
||||
}
|
||||
$validator->validate();
|
||||
return;
|
||||
}
|
||||
|
||||
if ($config->rules === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
Validator::make($data, $config->rules, $config->messages, $config->attributes)->validate();
|
||||
}
|
||||
|
||||
private function getRequestData(Request $request): array
|
||||
{
|
||||
$routeParams = $request->route ? $request->route->param() : [];
|
||||
if (!is_array($routeParams)) {
|
||||
$routeParams = [];
|
||||
}
|
||||
return array_merge($request->all() ?: [], $routeParams);
|
||||
}
|
||||
|
||||
private function resolveRequestData(Request $request, string|array|null $in, array $defaultData): array
|
||||
{
|
||||
if ($in === null || $in === []) {
|
||||
return $defaultData;
|
||||
}
|
||||
|
||||
$parts = is_array($in) ? $in : [$in];
|
||||
$data = [];
|
||||
foreach ($parts as $part) {
|
||||
$data = array_merge($data, $this->getRequestPartData($request, $part));
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function getRequestPartData(Request $request, mixed $part): array
|
||||
{
|
||||
if (!is_string($part) || $part === '') {
|
||||
throw new InvalidArgumentException('Validate/Param in must be a non-empty string or string array.');
|
||||
}
|
||||
|
||||
return match ($part) {
|
||||
'query' => $request->get() ?: [],
|
||||
'body' => $request->post() ?: [],
|
||||
'path' => $this->getPathParams($request),
|
||||
default => throw new InvalidArgumentException("Unsupported in value: {$part}. Only query|body|path are supported."),
|
||||
};
|
||||
}
|
||||
|
||||
private function getPathParams(Request $request): array
|
||||
{
|
||||
$routeParams = $request->route ? $request->route->param() : [];
|
||||
return is_array($routeParams) ? $routeParams : [];
|
||||
}
|
||||
|
||||
private function getMethodMetadata(string $controller, string $action): ?array
|
||||
{
|
||||
$key = $controller . '::' . $action;
|
||||
if (isset(self::$metadataCache[$key])) {
|
||||
return self::$metadataCache[$key];
|
||||
}
|
||||
|
||||
if (!method_exists($controller, $action)) {
|
||||
return self::$metadataCache[$key] = null;
|
||||
}
|
||||
|
||||
$reflection = new ReflectionMethod($controller, $action);
|
||||
|
||||
return self::$metadataCache[$key] = $this->buildMetadataFromReflection($reflection, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve metadata for closure / named-function route handlers.
|
||||
*/
|
||||
private function getCallableMetadata(Request $request): ?array
|
||||
{
|
||||
$route = $request->route;
|
||||
if (!$route || !method_exists($route, 'getCallback')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$callback = $route->getCallback();
|
||||
|
||||
// Only handle closures and named function strings.
|
||||
if (!$callback instanceof Closure && !is_string($callback)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_string($callback) && !function_exists($callback)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$cacheKey = is_string($callback)
|
||||
? 'func::' . $callback
|
||||
: 'callable::' . $route->getPath();
|
||||
|
||||
if (isset(self::$metadataCache[$cacheKey])) {
|
||||
return self::$metadataCache[$cacheKey];
|
||||
}
|
||||
|
||||
$reflection = new ReflectionFunction($callback);
|
||||
|
||||
// Named functions support function-level #[Validate]; closures do not.
|
||||
$supportsMethodAttributes = is_string($callback);
|
||||
|
||||
return self::$metadataCache[$cacheKey] = $this->buildMetadataFromReflection(
|
||||
$reflection,
|
||||
$supportsMethodAttributes
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build validation metadata from a ReflectionMethod or ReflectionFunction.
|
||||
*/
|
||||
private function buildMetadataFromReflection(
|
||||
ReflectionFunctionAbstract $reflection,
|
||||
bool $supportsMethodAttributes
|
||||
): array {
|
||||
$methods = [];
|
||||
if ($supportsMethodAttributes) {
|
||||
foreach ($reflection->getAttributes(Validate::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
|
||||
$methods[] = $attribute->newInstance();
|
||||
}
|
||||
}
|
||||
|
||||
$parameters = $reflection->getParameters();
|
||||
$hasAnyParamAttribute = false;
|
||||
foreach ($parameters as $parameter) {
|
||||
if ($parameter->getAttributes(Param::class, \ReflectionAttribute::IS_INSTANCEOF) !== []) {
|
||||
$hasAnyParamAttribute = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$inferWhenAnnotationsPresent = $methods !== [] || $hasAnyParamAttribute;
|
||||
|
||||
$params = [];
|
||||
foreach ($parameters as $parameter) {
|
||||
$paramConfig = $this->resolveParamConfig($parameter, $inferWhenAnnotationsPresent);
|
||||
if ($paramConfig === null) {
|
||||
continue;
|
||||
}
|
||||
$hasDefault = $parameter->isDefaultValueAvailable();
|
||||
$params[] = [
|
||||
'name' => $parameter->getName(),
|
||||
'config' => $paramConfig,
|
||||
'hasDefault' => $hasDefault,
|
||||
'default' => $hasDefault ? $parameter->getDefaultValue() : null,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'has' => $methods !== [] || $params !== [],
|
||||
'methods' => $methods,
|
||||
'params' => $params,
|
||||
];
|
||||
}
|
||||
|
||||
private function resolveParamConfig(ReflectionParameter $parameter, bool $inferWhenAnnotationsPresent): ?Param
|
||||
{
|
||||
$attributes = $parameter->getAttributes(Param::class, \ReflectionAttribute::IS_INSTANCEOF);
|
||||
if ($attributes !== []) {
|
||||
/** @var Param $config */
|
||||
$config = $attributes[0]->newInstance();
|
||||
|
||||
// Auto-complete rules based on parameter signature.
|
||||
$completedRules = $this->completeRulesFromParameter($parameter, $config->rules);
|
||||
if ($completedRules !== $config->rules) {
|
||||
return new Param(
|
||||
rules: $completedRules,
|
||||
messages: $config->messages,
|
||||
attribute: $config->attribute,
|
||||
in: $config->in
|
||||
);
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
if (!$inferWhenAnnotationsPresent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->shouldSkipParameter($parameter)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$rules = $this->inferRulesFromParameter($parameter);
|
||||
return new Param(rules: $rules);
|
||||
}
|
||||
|
||||
private function shouldSkipParameter(ReflectionParameter $parameter): bool
|
||||
{
|
||||
$type = $parameter->getType();
|
||||
if (!$type instanceof ReflectionNamedType || $type->isBuiltin()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$name = $type->getName();
|
||||
if ($name === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Skip framework request injection.
|
||||
if (is_a($name, Request::class, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Skip other class-typed parameters by default (services/DTOs/etc).
|
||||
return true;
|
||||
}
|
||||
|
||||
private function inferRulesFromParameter(ReflectionParameter $parameter): string|array
|
||||
{
|
||||
$rules = [];
|
||||
|
||||
$type = $parameter->getType();
|
||||
$isNullable = $type instanceof ReflectionNamedType && $type->allowsNull();
|
||||
|
||||
// Required when: no default value AND not nullable.
|
||||
if (!$parameter->isDefaultValueAvailable() && !$isNullable) {
|
||||
$rules[] = 'required';
|
||||
}
|
||||
|
||||
if ($type instanceof ReflectionUnionType) {
|
||||
// Union types are not inferred by default (developer can explicitly use #[Param]).
|
||||
return implode('|', $rules);
|
||||
}
|
||||
|
||||
if ($type instanceof ReflectionNamedType && $type->isBuiltin()) {
|
||||
$mapped = $this->mapBuiltinTypeToRule($type->getName());
|
||||
if ($mapped !== '') {
|
||||
$rules[] = $mapped;
|
||||
}
|
||||
if ($isNullable) {
|
||||
$rules[] = 'nullable';
|
||||
}
|
||||
}
|
||||
|
||||
return implode('|', $rules);
|
||||
}
|
||||
|
||||
private function completeRulesFromParameter(ReflectionParameter $parameter, string|array $existingRules): string|array
|
||||
{
|
||||
$isArray = is_array($existingRules);
|
||||
$rulesList = $isArray
|
||||
? $existingRules
|
||||
: ($existingRules !== '' ? explode('|', $existingRules) : []);
|
||||
|
||||
$type = $parameter->getType();
|
||||
$isNullable = $type instanceof ReflectionNamedType && $type->allowsNull();
|
||||
|
||||
// Build rule name set for O(1) lookups instead of iterating per check.
|
||||
$ruleNames = [];
|
||||
foreach ($rulesList as $rule) {
|
||||
$ruleNames[explode(':', $rule, 2)[0]] = true;
|
||||
}
|
||||
|
||||
// Auto-complete 'required' if: no default value, not nullable, and not already present.
|
||||
if (!$parameter->isDefaultValueAvailable() && !$isNullable && !isset($ruleNames['required'])) {
|
||||
array_unshift($rulesList, 'required');
|
||||
$ruleNames['required'] = true;
|
||||
}
|
||||
|
||||
// Auto-complete type rule if: has builtin type and no type rule present.
|
||||
if ($type instanceof ReflectionNamedType && $type->isBuiltin()) {
|
||||
$mappedRule = $this->mapBuiltinTypeToRule($type->getName());
|
||||
if ($mappedRule !== '' && !isset($ruleNames[$mappedRule])) {
|
||||
// Insert type rule after 'required' if present, otherwise at the beginning.
|
||||
$requiredIndex = array_search('required', $rulesList, true);
|
||||
if ($requiredIndex !== false) {
|
||||
array_splice($rulesList, $requiredIndex + 1, 0, $mappedRule);
|
||||
} else {
|
||||
array_unshift($rulesList, $mappedRule);
|
||||
}
|
||||
$ruleNames[$mappedRule] = true;
|
||||
}
|
||||
|
||||
// Auto-complete 'nullable' if: type is nullable and not already present.
|
||||
if ($isNullable && !isset($ruleNames['nullable'])) {
|
||||
$rulesList[] = 'nullable';
|
||||
}
|
||||
}
|
||||
|
||||
return $isArray ? $rulesList : implode('|', $rulesList);
|
||||
}
|
||||
|
||||
private function mapBuiltinTypeToRule(string $type): string
|
||||
{
|
||||
return match ($type) {
|
||||
'string' => 'string',
|
||||
'int' => 'integer',
|
||||
'float' => 'numeric',
|
||||
'bool' => 'boolean',
|
||||
'array' => 'array',
|
||||
default => '',
|
||||
};
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webman\Validation\Tests;
|
||||
|
||||
use Illuminate\Validation\Rule as IlluminateRule;
|
||||
use Illuminate\Validation\Rules\In;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use support\validation\Rule;
|
||||
use support\validation\Validator;
|
||||
use support\validation\ValidationException;
|
||||
|
||||
final class RuleTest extends TestCase
|
||||
{
|
||||
public function testRuleExtendsIlluminateRule(): void
|
||||
{
|
||||
$this->assertTrue(is_subclass_of(Rule::class, IlluminateRule::class));
|
||||
}
|
||||
|
||||
public function testRuleInProxy(): void
|
||||
{
|
||||
$rule = Rule::in(['a', 'b']);
|
||||
$this->assertInstanceOf(In::class, $rule);
|
||||
}
|
||||
|
||||
public function testRuleInPassesValidation(): void
|
||||
{
|
||||
$validated = Validator::make(
|
||||
['status' => 'enabled'],
|
||||
['status' => ['required', Rule::in(['enabled', 'disabled'])]]
|
||||
)->validate();
|
||||
|
||||
$this->assertSame(['status' => 'enabled'], $validated);
|
||||
}
|
||||
|
||||
public function testRuleInFailsValidation(): void
|
||||
{
|
||||
try {
|
||||
Validator::make(
|
||||
['status' => 'unknown'],
|
||||
['status' => ['required', Rule::in(['enabled', 'disabled'])]],
|
||||
['status.in' => 'Status invalid']
|
||||
)->validate();
|
||||
$this->fail('Expected ValidationException was not thrown.');
|
||||
} catch (ValidationException $exception) {
|
||||
$this->assertSame('Status invalid', $exception->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function testRuleAnyOfPassesValidation(): void
|
||||
{
|
||||
$validated = Validator::make(
|
||||
['username' => 'user@example.com'],
|
||||
['username' => [
|
||||
'required',
|
||||
Rule::anyOf([
|
||||
['string', 'email'],
|
||||
['string', 'alpha_dash', 'min:6'],
|
||||
]),
|
||||
]]
|
||||
)->validate();
|
||||
|
||||
$this->assertSame(['username' => 'user@example.com'], $validated);
|
||||
}
|
||||
|
||||
public function testRuleAnyOfFailsValidation(): void
|
||||
{
|
||||
try {
|
||||
Validator::make(
|
||||
['username' => 'a?'],
|
||||
['username' => [
|
||||
'required',
|
||||
Rule::anyOf([
|
||||
['string', 'email'],
|
||||
['string', 'alpha_dash', 'min:6'],
|
||||
]),
|
||||
]],
|
||||
['username.any_of' => 'Username invalid']
|
||||
)->validate();
|
||||
$this->fail('Expected ValidationException was not thrown.');
|
||||
} catch (ValidationException $exception) {
|
||||
$this->assertSame('Username invalid', $exception->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webman\Validation\Tests;
|
||||
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionProperty;
|
||||
use Webman\Validation\Factory\ValidationFactory;
|
||||
use support\validation\Validator;
|
||||
use support\validation\ValidationException;
|
||||
|
||||
final class TranslationTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @return array<string, array{string, string}>
|
||||
*/
|
||||
public static function localeEmailMessageProvider(): array
|
||||
{
|
||||
return [
|
||||
'zh_CN' => ['zh_CN', '必须是有效的邮箱地址'],
|
||||
'zh_TW' => ['zh_TW', '必須是有效的電子郵件地址'],
|
||||
'en' => ['en', 'must be a valid email address'],
|
||||
'ja' => ['ja', '有効なメールアドレスである必要があります'],
|
||||
'ko' => ['ko', '유효한 이메일 주소여야 합니다'],
|
||||
'fr' => ['fr', 'doit être une adresse e-mail valide'],
|
||||
'de' => ['de', 'muss eine gültige E-Mail-Adresse sein'],
|
||||
'es' => ['es', 'debe ser una dirección de correo electrónico válida'],
|
||||
'pt_BR' => ['pt_BR', 'deve ser um endereço de e-mail válido'],
|
||||
'ru' => ['ru', 'должно быть действительным адресом электронной почты'],
|
||||
'vi' => ['vi', 'phải là địa chỉ email hợp lệ'],
|
||||
'tr' => ['tr', 'geçerli bir e-posta adresi olmalıdır'],
|
||||
'id' => ['id', 'harus berupa alamat email yang valid'],
|
||||
'th' => ['th', 'ต้องเป็นที่อยู่อีเมลที่ถูกต้อง'],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('localeEmailMessageProvider')]
|
||||
public function testValidationMessageInLocale(string $locale, string $expectedSubstring): void
|
||||
{
|
||||
$packageLangPath = dirname(__DIR__, 2) . '/resources/lang';
|
||||
if (!is_dir($packageLangPath) || !is_dir($packageLangPath . '/' . $locale)) {
|
||||
$this->markTestSkipped("Locale {$locale} not found in package resources/lang.");
|
||||
}
|
||||
|
||||
validation_test_set_config([
|
||||
'translation' => [
|
||||
'path' => $packageLangPath,
|
||||
'locale' => $locale,
|
||||
'fallback_locale' => [$locale],
|
||||
],
|
||||
]);
|
||||
if (function_exists('locale')) {
|
||||
locale($locale);
|
||||
}
|
||||
$this->resetTranslationInstance();
|
||||
$this->resetFactory();
|
||||
|
||||
try {
|
||||
Validator::make(['email' => 'bad-email'], ['email' => 'required|email'])->validate();
|
||||
$this->fail('Expected ValidationException was not thrown.');
|
||||
} catch (ValidationException $exception) {
|
||||
$this->assertStringContainsString(
|
||||
$expectedSubstring,
|
||||
$exception->getMessage(),
|
||||
"Locale {$locale}: expected message to contain '{$expectedSubstring}'"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function testLocalTranslationsOverridePackage(): void
|
||||
{
|
||||
if (!class_exists(\Symfony\Component\Translation\Translator::class)) {
|
||||
$this->markTestSkipped('symfony/translation is not installed.');
|
||||
}
|
||||
|
||||
validation_test_set_config([
|
||||
'translation' => [
|
||||
'path' => $this->fixturePath('translations'),
|
||||
'locale' => 'zh_CN',
|
||||
'fallback_locale' => ['zh_CN'],
|
||||
],
|
||||
]);
|
||||
if (function_exists('locale')) {
|
||||
locale('zh_CN');
|
||||
}
|
||||
$this->resetTranslationInstance();
|
||||
$this->resetFactory();
|
||||
|
||||
try {
|
||||
Validator::make(['email' => 'bad-email'], ['email' => 'required|email'])->validate();
|
||||
$this->fail('Expected ValidationException was not thrown.');
|
||||
} catch (ValidationException $exception) {
|
||||
$this->assertSame('[LOCAL_ZH] email invalid.', $exception->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function testFallbackToPackageTranslations(): void
|
||||
{
|
||||
validation_test_set_config([
|
||||
'translation' => [
|
||||
'path' => $this->fixturePath('empty'),
|
||||
'locale' => 'en',
|
||||
'fallback_locale' => ['en'],
|
||||
],
|
||||
]);
|
||||
if (function_exists('locale')) {
|
||||
locale('en');
|
||||
}
|
||||
$this->resetTranslationInstance();
|
||||
$this->resetFactory();
|
||||
|
||||
try {
|
||||
Validator::make(['email' => 'bad-email'], ['email' => 'required|email'])->validate();
|
||||
$this->fail('Expected ValidationException was not thrown.');
|
||||
} catch (ValidationException $exception) {
|
||||
$this->assertSame('The email field must be a valid email address.', $exception->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function resetFactory(): void
|
||||
{
|
||||
$property = new ReflectionProperty(ValidationFactory::class, 'factory');
|
||||
$property->setAccessible(true);
|
||||
$property->setValue(null, null);
|
||||
}
|
||||
|
||||
private function resetTranslationInstance(): void
|
||||
{
|
||||
if (!class_exists(\support\Translation::class)) {
|
||||
return;
|
||||
}
|
||||
$property = new ReflectionProperty(\support\Translation::class, 'instance');
|
||||
$property->setAccessible(true);
|
||||
$property->setValue(null, []);
|
||||
}
|
||||
|
||||
private function fixturePath(string $name): string
|
||||
{
|
||||
return __DIR__ . '/fixtures/' . $name;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webman\Validation\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use support\validation\Validator as SupportValidator;
|
||||
use support\validation\ValidationException as SupportValidationException;
|
||||
use Webman\Validation\Validator as CoreValidator;
|
||||
|
||||
final class ValidatorExceptionTest extends TestCase
|
||||
{
|
||||
public function test_core_validator_uses_support_validation_exception_by_default(): void
|
||||
{
|
||||
$this->expectException(SupportValidationException::class);
|
||||
|
||||
CoreValidator::make(
|
||||
['name' => null],
|
||||
['name' => 'required']
|
||||
)->validate();
|
||||
}
|
||||
|
||||
public function test_support_validator_uses_support_validation_exception_by_default(): void
|
||||
{
|
||||
$this->expectException(SupportValidationException::class);
|
||||
|
||||
SupportValidator::make(
|
||||
['name' => null],
|
||||
['name' => 'required']
|
||||
)->validate();
|
||||
}
|
||||
|
||||
public function test_exception_override_uses_custom_exception(): void
|
||||
{
|
||||
$this->expectException(\RuntimeException::class);
|
||||
|
||||
CoreValidator::make(
|
||||
['name' => null],
|
||||
['name' => 'required']
|
||||
)
|
||||
->withException(\RuntimeException::class)
|
||||
->validate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webman\Validation\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use support\validation\ValidationException;
|
||||
use support\validation\Validator as SupportValidator;
|
||||
|
||||
final class ValidatorPublicMethodsTest extends TestCase
|
||||
{
|
||||
public function testOverrideRulesMethodIsUsed(): void
|
||||
{
|
||||
$validator = OverrideRulesValidator::make(['email' => 'not-an-email']);
|
||||
|
||||
$this->expectException(ValidationException::class);
|
||||
$validator->validate();
|
||||
}
|
||||
|
||||
public function testOverrideMessagesMethodIsUsed(): void
|
||||
{
|
||||
$validator = OverrideMessagesValidator::make([]);
|
||||
|
||||
$this->expectException(ValidationException::class);
|
||||
$this->expectExceptionMessage('Custom required message');
|
||||
$validator->validate();
|
||||
}
|
||||
|
||||
public function testOverrideAttributesMethodIsUsed(): void
|
||||
{
|
||||
$validator = OverrideAttributesValidator::make([]);
|
||||
|
||||
$this->expectException(ValidationException::class);
|
||||
$this->expectExceptionMessage('E-mail is required');
|
||||
$validator->validate();
|
||||
}
|
||||
|
||||
public function testOverrideDataMethodIsUsed(): void
|
||||
{
|
||||
$validated = OverrideDataValidator::make(['email' => 'not-an-email'])->validate();
|
||||
$this->assertSame(['email' => 'user@example.com'], $validated);
|
||||
}
|
||||
|
||||
public function testReuseRulesFromAnotherValidatorViaPublicMethod(): void
|
||||
{
|
||||
$validated = ReuseRulesValidator::make(['email' => 'user@example.com'])->validate();
|
||||
$this->assertSame(['email' => 'user@example.com'], $validated);
|
||||
}
|
||||
}
|
||||
|
||||
final class OverrideRulesValidator extends SupportValidator
|
||||
{
|
||||
protected array $rules = [
|
||||
'dummy' => 'nullable',
|
||||
];
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => 'required|email',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
final class OverrideMessagesValidator extends SupportValidator
|
||||
{
|
||||
protected array $rules = [
|
||||
'email' => 'required',
|
||||
];
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'email.required' => 'Custom required message',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
final class OverrideAttributesValidator extends SupportValidator
|
||||
{
|
||||
protected array $rules = [
|
||||
'email' => 'required',
|
||||
];
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'email.required' => ':attribute is required',
|
||||
];
|
||||
}
|
||||
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'email' => 'E-mail',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
final class OverrideDataValidator extends SupportValidator
|
||||
{
|
||||
protected array $rules = [
|
||||
'email' => 'required|email',
|
||||
];
|
||||
|
||||
public function data(): array
|
||||
{
|
||||
return [
|
||||
'email' => 'user@example.com',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
final class RulesSourceValidator extends SupportValidator
|
||||
{
|
||||
protected array $rules = [
|
||||
'email' => 'required|email',
|
||||
];
|
||||
}
|
||||
|
||||
final class ReuseRulesValidator extends SupportValidator
|
||||
{
|
||||
protected array $rules = [
|
||||
'dummy' => 'nullable',
|
||||
];
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return RulesSourceValidator::make($this->data)->rules();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webman\Validation\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use support\validation\Validator;
|
||||
use support\validation\ValidationException;
|
||||
use Webman\Validation\Exception\ValidationException as BaseValidationException;
|
||||
|
||||
final class ValidatorTest extends TestCase
|
||||
{
|
||||
public function testValidatePassReturnsValidatedData(): void
|
||||
{
|
||||
$validated = Validator::make(
|
||||
['email' => 'user@example.com'],
|
||||
['email' => 'required|email']
|
||||
)->validate();
|
||||
|
||||
$this->assertSame(['email' => 'user@example.com'], $validated);
|
||||
}
|
||||
|
||||
public function testValidateFailThrowsConfiguredExceptionWithFirstMessage(): void
|
||||
{
|
||||
try {
|
||||
Validator::make(
|
||||
['email' => 'not-an-email'],
|
||||
['email' => 'required|email'],
|
||||
['email.email' => 'Email invalid']
|
||||
)->validate();
|
||||
$this->fail('Expected ValidationException was not thrown.');
|
||||
} catch (ValidationException $exception) {
|
||||
$this->assertSame('Email invalid', $exception->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function testValidateFailUsesConfigExceptionClass(): void
|
||||
{
|
||||
$this->setValidationExceptionConfig(ConfigValidationException::class);
|
||||
|
||||
try {
|
||||
Validator::make(
|
||||
['email' => 'not-an-email'],
|
||||
['email' => 'required|email']
|
||||
)->validate();
|
||||
$this->fail('Expected ConfigValidationException was not thrown.');
|
||||
} catch (ConfigValidationException $exception) {
|
||||
$this->assertSame('The email field must be a valid email address.', $exception->getMessage());
|
||||
} finally {
|
||||
$this->setValidationExceptionConfig(ValidationException::class);
|
||||
}
|
||||
}
|
||||
|
||||
public function testCustomValidatorWithoutScenesUsesAllRulesByDefault(): void
|
||||
{
|
||||
$validated = UserValidatorWithoutScenes::make([
|
||||
'email' => 'user@example.com',
|
||||
])->validate();
|
||||
|
||||
$this->assertSame(['email' => 'user@example.com'], $validated);
|
||||
}
|
||||
|
||||
// ───── Closure as validation rule tests ─────
|
||||
|
||||
public function testClosureRuleValidationPasses(): void
|
||||
{
|
||||
$validated = Validator::make(
|
||||
['age' => 20],
|
||||
['age' => ['required', 'integer', function ($attribute, $value, $fail) {
|
||||
if ($value < 18) {
|
||||
$fail("The {$attribute} must be at least 18.");
|
||||
}
|
||||
}]]
|
||||
)->validate();
|
||||
|
||||
$this->assertSame(['age' => 20], $validated);
|
||||
}
|
||||
|
||||
public function testClosureRuleValidationFails(): void
|
||||
{
|
||||
try {
|
||||
Validator::make(
|
||||
['age' => 15],
|
||||
['age' => ['required', 'integer', function ($attribute, $value, $fail) {
|
||||
if ($value < 18) {
|
||||
$fail("The {$attribute} must be at least 18.");
|
||||
}
|
||||
}]]
|
||||
)->validate();
|
||||
$this->fail('Expected ValidationException was not thrown.');
|
||||
} catch (ValidationException $exception) {
|
||||
$this->assertSame('The age must be at least 18.', $exception->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function testMultipleClosureRulesValidation(): void
|
||||
{
|
||||
$validated = Validator::make(
|
||||
['code' => 'prefix_abc'],
|
||||
['code' => ['required', 'string',
|
||||
function ($attribute, $value, $fail) {
|
||||
if (!str_starts_with($value, 'prefix_')) {
|
||||
$fail("The {$attribute} must start with prefix_.");
|
||||
}
|
||||
},
|
||||
function ($attribute, $value, $fail) {
|
||||
if (strlen($value) < 5) {
|
||||
$fail("The {$attribute} must be at least 5 characters.");
|
||||
}
|
||||
},
|
||||
]]
|
||||
)->validate();
|
||||
|
||||
$this->assertSame(['code' => 'prefix_abc'], $validated);
|
||||
}
|
||||
|
||||
public function testClosureRuleInCustomValidator(): void
|
||||
{
|
||||
$validated = ClosureRuleValidator::make(['score' => 85])->validate();
|
||||
$this->assertSame(['score' => 85], $validated);
|
||||
}
|
||||
|
||||
public function testClosureRuleInCustomValidatorFails(): void
|
||||
{
|
||||
try {
|
||||
ClosureRuleValidator::make(['score' => 150])->validate();
|
||||
$this->fail('Expected ValidationException was not thrown.');
|
||||
} catch (ValidationException $exception) {
|
||||
$this->assertStringContainsString('score', $exception->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ───── Helpers ─────
|
||||
|
||||
private function setValidationExceptionConfig(string $exceptionClass): void
|
||||
{
|
||||
validation_test_set_config([
|
||||
'plugin' => [
|
||||
'webman' => [
|
||||
'validation' => [
|
||||
'app' => [
|
||||
'exception' => $exceptionClass,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
final class ConfigValidationException extends BaseValidationException
|
||||
{
|
||||
}
|
||||
|
||||
final class UserValidatorWithoutScenes extends Validator
|
||||
{
|
||||
protected array $rules = [
|
||||
'email' => 'required|email',
|
||||
];
|
||||
}
|
||||
|
||||
final class ClosureRuleValidator extends Validator
|
||||
{
|
||||
protected array $rules = [
|
||||
'score' => 'required|integer',
|
||||
];
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'score' => ['required', 'integer', function ($attribute, $value, $fail) {
|
||||
if ($value < 0 || $value > 100) {
|
||||
$fail("The {$attribute} must be between 0 and 100.");
|
||||
}
|
||||
}],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
if (!function_exists('validation_test_set_config')) {
|
||||
function validation_test_set_config(array $overrides = []): void
|
||||
{
|
||||
$defaults = [
|
||||
'container' => new \Webman\Container(),
|
||||
'translation' => [
|
||||
'locale' => 'en',
|
||||
'fallback_locale' => ['en'],
|
||||
'path' => '',
|
||||
],
|
||||
'plugin' => [
|
||||
'webman' => [
|
||||
'validation' => [
|
||||
'app' => [
|
||||
'enable' => true,
|
||||
'exception' => \support\validation\ValidationException::class,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$current = $GLOBALS['validation_test_config'] ?? [];
|
||||
$merged = array_replace_recursive($defaults, $current, $overrides);
|
||||
$GLOBALS['validation_test_config'] = $merged;
|
||||
|
||||
if (class_exists(\Webman\Config::class)) {
|
||||
$property = new ReflectionProperty(\Webman\Config::class, 'config');
|
||||
$property->setAccessible(true);
|
||||
$property->setValue(null, $merged);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validation_test_set_config();
|
||||
|
||||
if (!function_exists('config')) {
|
||||
function config(string $key, mixed $default = null): mixed
|
||||
{
|
||||
$config = $GLOBALS['validation_test_config'] ?? [];
|
||||
if ($key === null || $key === '') {
|
||||
return $config;
|
||||
}
|
||||
|
||||
$parts = explode('.', $key);
|
||||
$value = $config;
|
||||
foreach ($parts as $part) {
|
||||
if (!is_array($value) || !array_key_exists($part, $value)) {
|
||||
return $default;
|
||||
}
|
||||
$value = $value[$part];
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'email' => '[LOCAL_EN] :attribute invalid.',
|
||||
];
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'email' => '[LOCAL_ZH] :attribute invalid.',
|
||||
];
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webman\Validation;
|
||||
|
||||
use Illuminate\Support\MessageBag;
|
||||
use Illuminate\Validation\Validator as IlluminateValidator;
|
||||
use InvalidArgumentException;
|
||||
use support\Container;
|
||||
use Throwable;
|
||||
use Webman\Validation\Factory\ValidationFactory;
|
||||
|
||||
class Validator
|
||||
{
|
||||
public static function make(
|
||||
array $data,
|
||||
?array $rules = null,
|
||||
?array $messages = null,
|
||||
?array $attributes = null
|
||||
): static {
|
||||
/** @var static $instance */
|
||||
$instance = Container::make(static::class);
|
||||
$instance->data = $data;
|
||||
|
||||
$instance->rules = $rules ?? $instance->rules;
|
||||
$instance->messages = $messages ?? $instance->messages;
|
||||
$instance->attributes = $attributes ?? $instance->attributes;
|
||||
|
||||
if ($instance->rules === []) {
|
||||
throw new InvalidArgumentException('Validation rules cannot be empty.');
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
protected array $rules = [];
|
||||
protected array $messages = [];
|
||||
protected array $attributes = [];
|
||||
protected array $scenes = [];
|
||||
|
||||
protected array $data = [];
|
||||
protected ?string $scene = null;
|
||||
private ?IlluminateValidator $validator = null;
|
||||
private ?string $exceptionClass = null;
|
||||
private static array $validatedExceptionClasses = [];
|
||||
|
||||
public function withScene(string $scene): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->scene = $scene;
|
||||
$clone->validator = null;
|
||||
return $clone;
|
||||
}
|
||||
|
||||
public function withException(string $exceptionClass): static
|
||||
{
|
||||
if ($exceptionClass === '') {
|
||||
throw new InvalidArgumentException('Validation exception must be a non-empty class string.');
|
||||
}
|
||||
|
||||
$clone = clone $this;
|
||||
$clone->exceptionClass = $exceptionClass;
|
||||
$clone->validator = null;
|
||||
return $clone;
|
||||
}
|
||||
|
||||
public function validate(): array
|
||||
{
|
||||
$validator = $this->toIlluminate();
|
||||
if ($validator->fails()) {
|
||||
$exceptionClass = $this->resolveExceptionClass();
|
||||
$message = $validator->errors()->first() ?: 'Validation failed';
|
||||
throw new $exceptionClass($message, 400);
|
||||
}
|
||||
return $validator->validated();
|
||||
}
|
||||
|
||||
public function passes(): bool
|
||||
{
|
||||
return $this->toIlluminate()->passes();
|
||||
}
|
||||
|
||||
public function fails(): bool
|
||||
{
|
||||
return $this->toIlluminate()->fails();
|
||||
}
|
||||
|
||||
public function errors(): MessageBag
|
||||
{
|
||||
return $this->toIlluminate()->errors();
|
||||
}
|
||||
|
||||
public function validated(): array
|
||||
{
|
||||
return $this->toIlluminate()->validated();
|
||||
}
|
||||
|
||||
public function toIlluminate(): IlluminateValidator
|
||||
{
|
||||
if ($this->validator !== null) {
|
||||
return $this->validator;
|
||||
}
|
||||
|
||||
$factory = ValidationFactory::getFactory();
|
||||
$this->validator = $factory->make(
|
||||
$this->data(),
|
||||
$this->rules(),
|
||||
$this->messages(),
|
||||
$this->attributes()
|
||||
);
|
||||
return $this->validator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this in subclasses to build validation rules dynamically.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return $this->resolveRules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this in subclasses to build custom messages dynamically.
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return $this->messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this in subclasses to build custom attribute names dynamically.
|
||||
*/
|
||||
public function attributes(): array
|
||||
{
|
||||
return $this->attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose incoming validation data for subclasses.
|
||||
*/
|
||||
public function data(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose current scene for subclasses.
|
||||
*/
|
||||
protected function scene(): ?string
|
||||
{
|
||||
return $this->scene;
|
||||
}
|
||||
|
||||
public function __call(string $name, array $arguments): mixed
|
||||
{
|
||||
$validator = $this->toIlluminate();
|
||||
if (!method_exists($validator, $name)) {
|
||||
throw new InvalidArgumentException("Validator method not found: {$name}");
|
||||
}
|
||||
return $validator->{$name}(...$arguments);
|
||||
}
|
||||
|
||||
private function resolveRules(): array
|
||||
{
|
||||
$scene = $this->scene;
|
||||
if ($scene === null) {
|
||||
return $this->rules;
|
||||
}
|
||||
|
||||
if (!array_key_exists($scene, $this->scenes)) {
|
||||
throw new InvalidArgumentException("Validation scene not defined: {$scene}");
|
||||
}
|
||||
|
||||
$fields = $this->scenes[$scene];
|
||||
if (!is_array($fields) || $fields === []) {
|
||||
throw new InvalidArgumentException("Validation scene has no fields: {$scene}");
|
||||
}
|
||||
|
||||
$rules = array_intersect_key($this->rules, array_flip($fields));
|
||||
if ($rules === []) {
|
||||
throw new InvalidArgumentException("Validation rules not found for scene: {$scene}");
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
private function resolveExceptionClass(): string
|
||||
{
|
||||
$exceptionClass = $this->exceptionClass;
|
||||
if ($exceptionClass === null) {
|
||||
$exceptionClass = config(
|
||||
'plugin.webman.validation.app.exception',
|
||||
\support\validation\ValidationException::class
|
||||
);
|
||||
}
|
||||
|
||||
if (!is_string($exceptionClass) || $exceptionClass === '') {
|
||||
throw new InvalidArgumentException('Validation exception must be a non-empty class string.');
|
||||
}
|
||||
|
||||
// Cache validation result per class name to avoid repeated reflection checks.
|
||||
if (isset(self::$validatedExceptionClasses[$exceptionClass])) {
|
||||
return $exceptionClass;
|
||||
}
|
||||
|
||||
if (!class_exists($exceptionClass)) {
|
||||
throw new InvalidArgumentException("Validation exception class not found: {$exceptionClass}");
|
||||
}
|
||||
if (!is_subclass_of($exceptionClass, Throwable::class)) {
|
||||
throw new InvalidArgumentException("Validation exception must implement Throwable: {$exceptionClass}");
|
||||
}
|
||||
|
||||
self::$validatedExceptionClasses[$exceptionClass] = true;
|
||||
return $exceptionClass;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
use support\validation\ValidationException;
|
||||
|
||||
return [
|
||||
'enable' => true,
|
||||
'exception' => ValidationException::class,
|
||||
];
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Webman\Validation\Command\MakeValidatorCommand;
|
||||
|
||||
return [
|
||||
MakeValidatorCommand::class
|
||||
];
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
use Webman\Validation\Middleware;
|
||||
|
||||
return [
|
||||
'@' => [
|
||||
Middleware::class,
|
||||
],
|
||||
];
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit bootstrap="../../autoload.php"
|
||||
colors="true"
|
||||
verbose="true">
|
||||
<testsuites>
|
||||
<testsuite name="Validation Test Suite">
|
||||
<directory>Tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace support\validation;
|
||||
|
||||
use Illuminate\Validation\Rule as IlluminateRule;
|
||||
|
||||
class Rule extends IlluminateRule
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace support\validation;
|
||||
|
||||
class ValidationException extends \Webman\Validation\Exception\ValidationException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace support\validation;
|
||||
|
||||
class Validator extends \Webman\Validation\Validator
|
||||
{
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace support\validation\annotation;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_PARAMETER)]
|
||||
class Param extends \Webman\Validation\Annotation\Param
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace support\validation\annotation;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
|
||||
class Validate extends \Webman\Validation\Annotation\Validate
|
||||
{
|
||||
}
|
||||
Reference in New Issue
Block a user