feat(database): 添加用户角色权限系统及相关监控功能

- 创建用户表(users)包含基本信息和认证字段
- 创建角色表(roles)用于权限控制
- 创建权限表(permissions)定义系统权限
- 创建用户角色关联表(user_roles)建立用户与角色关系
- 创建角色权限关联表(role_permissions)建立角色与权限关系
- 创建迁移记录表(migrations)追踪数据库变更
- 添加AdminController提供管理员面板功能
- 实现系统监控、配置管理、缓存清理等功能
- 添加AOP切面编程支持的各种通知类型
- 实现告警管理AlertManager支持多渠道告警
- 添加文档注解接口规范
This commit is contained in:
Lawson
2026-04-08 17:00:28 +08:00
commit 2782d765fb
270 changed files with 107192 additions and 0 deletions

View File

@@ -0,0 +1,725 @@
<?php
declare(strict_types=1);
namespace Fendx\I18n\Tools\Extractor;
use Fendx\I18n\Tools\Extractor\Parser\PhpParser;
use Fendx\I18n\Tools\Extractor\Parser\JsParser;
use Fendx\I18n\Tools\Extractor\Parser\HtmlParser;
use Fendx\I18n\Tools\Extractor\Parser\TwigParser;
use Fendx\I18n\Tools\Extractor\Parser\VueParser;
class TranslationKeyExtractor
{
protected PhpParser $phpParser;
protected JsParser $jsParser;
protected HtmlParser $htmlParser;
protected TwigParser $twigParser;
protected VueParser $vueParser;
protected array $config = [];
protected array $extractedKeys = [];
protected array $fileScanned = [];
public function __construct(array $config = [])
{
$this->config = array_merge($this->getDefaultConfig(), $config);
$this->phpParser = new PhpParser($this->config);
$this->jsParser = new JsParser($this->config);
$this->htmlParser = new HtmlParser($this->config);
$this->twigParser = new TwigParser($this->config);
$this->vueParser = new VueParser($this->config);
}
/**
* Extract translation keys from directory.
*/
public function extractFromDirectory(string $directory, array $options = []): array
{
$this->extractedKeys = [];
$this->fileScanned = [];
$recursive = $options['recursive'] ?? true;
$patterns = $options['patterns'] ?? $this->config['file_patterns'];
$excludePatterns = $options['exclude_patterns'] ?? $this->config['exclude_patterns'];
$this->scanDirectory($directory, $recursive, $patterns, $excludePatterns);
return $this->getExtractedKeys();
}
/**
* Extract translation keys from file.
*/
public function extractFromFile(string $filepath): array
{
$this->extractedKeys = [];
$this->fileScanned = [];
if (!file_exists($filepath)) {
throw new \InvalidArgumentException("File not found: {$filepath}");
}
$this->scanFile($filepath);
return $this->getExtractedKeys();
}
/**
* Extract translation keys from string content.
*/
public function extractFromString(string $content, string $filename = 'unknown'): array
{
$this->extractedKeys = [];
$this->fileScanned = [];
$this->parseContent($content, $filename);
return $this->getExtractedKeys();
}
/**
* Scan directory for files.
*/
protected function scanDirectory(string $directory, bool $recursive, array $patterns, array $excludePatterns): void
{
$iterator = new \RecursiveIteratorIterator(
$recursive ? new \RecursiveDirectoryIterator($directory) : new \DirectoryIterator($directory),
\RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $file) {
if ($file->isDir()) {
continue;
}
$filepath = $file->getPathname();
// Check exclude patterns
if ($this->matchesExcludePatterns($filepath, $excludePatterns)) {
continue;
}
// Check include patterns
if ($this->matchesPatterns($filepath, $patterns)) {
$this->scanFile($filepath);
}
}
}
/**
* Scan single file.
*/
protected function scanFile(string $filepath): void
{
if (in_array($filepath, $this->fileScanned)) {
return;
}
$content = file_get_contents($filepath);
if ($content === false) {
return;
}
$this->fileScanned[] = $filepath;
$this->parseContent($content, $filepath);
}
/**
* Parse content based on file type.
*/
protected function parseContent(string $content, string $filename): void
{
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
switch ($extension) {
case 'php':
$this->extractFromPhp($content, $filename);
break;
case 'js':
case 'jsx':
case 'ts':
case 'tsx':
$this->extractFromJs($content, $filename);
break;
case 'html':
case 'htm':
$this->extractFromHtml($content, $filename);
break;
case 'twig':
$this->extractFromTwig($content, $filename);
break;
case 'vue':
$this->extractFromVue($content, $filename);
break;
default:
// Try to detect content type
if ($this->isPhpContent($content)) {
$this->extractFromPhp($content, $filename);
} elseif ($this->isJsContent($content)) {
$this->extractFromJs($content, $filename);
} elseif ($this->isHtmlContent($content)) {
$this->extractFromHtml($content, $filename);
} elseif ($this->isTwigContent($content)) {
$this->extractFromTwig($content, $filename);
}
break;
}
}
/**
* Extract from PHP content.
*/
protected function extractFromPhp(string $content, string $filename): void
{
$keys = $this->phpParser->extract($content);
$this->addExtractedKeys($keys, $filename, 'php');
}
/**
* Extract from JavaScript content.
*/
protected function extractFromJs(string $content, string $filename): void
{
$keys = $this->jsParser->extract($content);
$this->addExtractedKeys($keys, $filename, 'js');
}
/**
* Extract from HTML content.
*/
protected function extractFromHtml(string $content, string $filename): void
{
$keys = $this->htmlParser->extract($content);
$this->addExtractedKeys($keys, $filename, 'html');
}
/**
* Extract from Twig content.
*/
protected function extractFromTwig(string $content, string $filename): void
{
$keys = $this->twigParser->extract($content);
$this->addExtractedKeys($keys, $filename, 'twig');
}
/**
* Extract from Vue content.
*/
protected function extractFromVue(string $content, string $filename): void
{
$keys = $this->vueParser->extract($content);
$this->addExtractedKeys($keys, $filename, 'vue');
}
/**
* Add extracted keys.
*/
protected function addExtractedKeys(array $keys, string $filename, string $type): void
{
foreach ($keys as $key => $info) {
if (!isset($this->extractedKeys[$key])) {
$this->extractedKeys[$key] = [
'key' => $key,
'files' => [],
'contexts' => [],
'parameters' => [],
'types' => [],
'line_numbers' => []
];
}
$this->extractedKeys[$key]['files'][] = $filename;
$this->extractedKeys[$key]['types'][] = $type;
if (isset($info['context'])) {
$this->extractedKeys[$key]['contexts'][] = $info['context'];
}
if (isset($info['parameters'])) {
$this->extractedKeys[$key]['parameters'] = array_merge(
$this->extractedKeys[$key]['parameters'],
$info['parameters']
);
}
if (isset($info['line'])) {
$this->extractedKeys[$key]['line_numbers'][] = $filename . ':' . $info['line'];
}
}
}
/**
* Get extracted keys.
*/
public function getExtractedKeys(): array
{
// Remove duplicates and sort
$result = [];
foreach ($this->extractedKeys as $key => $info) {
$result[$key] = [
'key' => $key,
'files' => array_unique($info['files']),
'contexts' => array_unique($info['contexts']),
'parameters' => array_unique($info['parameters']),
'types' => array_unique($info['types']),
'line_numbers' => array_unique($info['line_numbers']),
'usage_count' => count($info['files'])
];
}
ksort($result);
return $result;
}
/**
* Get keys by file.
*/
public function getKeysByFile(): array
{
$keysByFile = [];
foreach ($this->extractedKeys as $key => $info) {
foreach ($info['files'] as $file) {
if (!isset($keysByFile[$file])) {
$keysByFile[$file] = [];
}
$keysByFile[$file][] = $key;
}
}
return $keysByFile;
}
/**
* Get keys by type.
*/
public function getKeysByType(): array
{
$keysByType = [];
foreach ($this->extractedKeys as $key => $info) {
foreach ($info['types'] as $type) {
if (!isset($keysByType[$type])) {
$keysByType[$type] = [];
}
$keysByType[$type][] = $key;
}
}
return $keysByType;
}
/**
* Get unused keys (keys not found in any file).
*/
public function getUnusedKeys(array $existingKeys): array
{
$extractedKeySet = array_keys($this->extractedKeys);
$unusedKeys = array_diff($existingKeys, $extractedKeySet);
return array_values($unusedKeys);
}
/**
* Get missing keys (keys found but not in existing translations).
*/
public function getMissingKeys(array $existingKeys): array
{
$extractedKeySet = array_keys($this->extractedKeys);
$missingKeys = array_diff($extractedKeySet, $existingKeys);
return array_values($missingKeys);
}
/**
* Get keys with parameters.
*/
public function getKeysWithParameters(): array
{
$keysWithParams = [];
foreach ($this->extractedKeys as $key => $info) {
if (!empty($info['parameters'])) {
$keysWithParams[$key] = array_unique($info['parameters']);
}
}
return $keysWithParams;
}
/**
* Get duplicate keys (keys found in multiple contexts).
*/
public function getDuplicateKeys(): array
{
$duplicates = [];
foreach ($this->extractedKeys as $key => $info) {
if (count($info['contexts']) > 1) {
$duplicates[$key] = [
'contexts' => array_unique($info['contexts']),
'files' => array_unique($info['files'])
];
}
}
return $duplicates;
}
/**
* Generate translation template.
*/
public function generateTemplate(array $existingKeys = null, string $language = 'en'): array
{
$template = [];
$extractedKeys = array_keys($this->extractedKeys);
if ($existingKeys) {
// Merge with existing keys
$allKeys = array_unique(array_merge($extractedKeys, $existingKeys));
} else {
$allKeys = $extractedKeys;
}
foreach ($allKeys as $key) {
$template[$key] = $this->generatePlaceholder($key, $language);
}
return $template;
}
/**
* Generate placeholder for translation key.
*/
protected function generatePlaceholder(string $key, string $language): string
{
$parts = explode('.', $key);
$lastPart = end($parts);
// Convert to human readable format
$placeholder = str_replace(['_', '-'], ' ', $lastPart);
$placeholder = ucwords($placeholder);
// Add language-specific prefix/suffix if needed
switch ($language) {
case 'zh-CN':
case 'zh-TW':
return $placeholder; // Chinese doesn't need articles
case 'ja':
return $placeholder; // Japanese doesn't need articles
case 'ko':
return $placeholder; // Korean doesn't need articles
case 'fr':
return "La {$placeholder}"; // French article
case 'de':
return "Der {$placeholder}"; // German article
case 'es':
return "El {$placeholder}"; // Spanish article
case 'it':
return "Il {$placeholder}"; // Italian article
case 'ru':
return "{$placeholder} (русский)"; // Russian suffix
case 'ar':
return "{$placeholder} (عربي)"; // Arabic suffix
default:
return "The {$placeholder}"; // English article
}
}
/**
* Export extracted keys.
*/
public function export(string $format = 'json'): string
{
$data = [
'extracted_keys' => $this->getExtractedKeys(),
'statistics' => $this->getStatistics(),
'generated_at' => date('Y-m-d H:i:s'),
'config' => $this->config
];
return match ($format) {
'json' => json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE),
'csv' => $this->exportToCsv(),
'xlsx' => $this->exportToXlsx(),
'php' => '<?php return ' . var_export($this->getExtractedKeys(), true) . ';',
default => throw new \InvalidArgumentException("Unsupported export format: {$format}")
};
}
/**
* Export to CSV format.
*/
protected function exportToCsv(): string
{
$csv = "Key,Files,Types,Contexts,Parameters,Usage Count\n";
foreach ($this->getExtractedKeys() as $key => $info) {
$csv .= '"' . $key . '",';
$csv .= '"' . implode('; ', $info['files']) . '",';
$csv .= '"' . implode('; ', $info['types']) . '",';
$csv .= '"' . implode('; ', $info['contexts']) . '",';
$csv .= '"' . implode('; ', $info['parameters']) . '",';
$csv .= $info['usage_count'] . "\n";
}
return $csv;
}
/**
* Export to XLSX format (basic implementation).
*/
protected function exportToXlsx(): string
{
// This would require a proper XLSX library
// For now, return CSV as placeholder
return $this->exportToCsv();
}
/**
* Get extraction statistics.
*/
public function getStatistics(): array
{
$keysByType = $this->getKeysByType();
$keysByFile = $this->getKeysByFile();
return [
'total_keys' => count($this->extractedKeys),
'total_files_scanned' => count($this->fileScanned),
'keys_by_type' => array_map('count', $keysByType),
'keys_by_file' => array_map('count', $keysByFile),
'keys_with_parameters' => count($this->getKeysWithParameters()),
'duplicate_keys' => count($this->getDuplicateKeys()),
'most_used_keys' => $this->getMostUsedKeys(10),
'file_types' => array_unique(array_merge(...array_values($keysByType)))
];
}
/**
* Get most used keys.
*/
protected function getMostUsedKeys(int $limit = 10): array
{
$keys = $this->getExtractedKeys();
uasort($keys, fn($a, $b) => $b['usage_count'] - $a['usage_count']);
return array_slice($keys, 0, $limit, true);
}
/**
* Check if file matches patterns.
*/
protected function matchesPatterns(string $filepath, array $patterns): bool
{
foreach ($patterns as $pattern) {
if (fnmatch($pattern, basename($filepath)) || fnmatch($pattern, $filepath)) {
return true;
}
}
return false;
}
/**
* Check if file matches exclude patterns.
*/
protected function matchesExcludePatterns(string $filepath, array $patterns): bool
{
foreach ($patterns as $pattern) {
if (fnmatch($pattern, basename($filepath)) || fnmatch($pattern, $filepath)) {
return true;
}
}
return false;
}
/**
* Check if content is PHP.
*/
protected function isPhpContent(string $content): bool
{
return str_contains($content, '<?php') ||
preg_match('/\b(?:function|class|interface|trait|namespace|use|require|include)\b/', $content);
}
/**
* Check if content is JavaScript.
*/
protected function isJsContent(string $content): bool
{
return preg_match('/\b(?:function|const|let|var|import|export|class|extends)\b/', $content) ||
str_contains($content, '=>') ||
str_contains($content, 'React');
}
/**
* Check if content is HTML.
*/
protected function isHtmlContent(string $content): bool
{
return preg_match('/<[^>]+>/', $content) &&
(str_contains($content, '<html') || str_contains($content, '<div') || str_contains($content, '<span'));
}
/**
* Check if content is Twig.
*/
protected function isTwigContent(string $content): bool
{
return preg_match('/\{\{.*?\}\}|\{%.*?%\}/', $content);
}
/**
* Add custom parser.
*/
public function addParser(string $extension, callable $parser): void
{
$this->customParsers[$extension] = $parser;
}
/**
* Set custom patterns.
*/
public function setPatterns(array $patterns): void
{
$this->config['file_patterns'] = $patterns;
}
/**
* Set exclude patterns.
*/
public function setExcludePatterns(array $patterns): void
{
$this->config['exclude_patterns'] = $patterns;
}
/**
* Reset extractor state.
*/
public function reset(): void
{
$this->extractedKeys = [];
$this->fileScanned = [];
}
/**
* Get default configuration.
*/
protected function getDefaultConfig(): array
{
return [
'file_patterns' => [
'*.php',
'*.js',
'*.jsx',
'*.ts',
'*.tsx',
'*.html',
'*.htm',
'*.twig',
'*.vue'
],
'exclude_patterns' => [
'vendor/*',
'node_modules/*',
'.git/*',
'storage/*',
'bootstrap/*',
'config/*',
'routes/*',
'tests/*',
'*.min.js',
'*.min.css',
'*.cache'
],
'php_patterns' => [
'/trans\s*\(\s*[\'"]([^\'"]+)[\'"]/',
'/__\s*\(\s*[\'"]([^\'"]+)[\'"]/',
'/t\s*\(\s*[\'"]([^\'"]+)[\'"]/',
'/translate\s*\(\s*[\'"]([^\'"]+)[\'"]/',
'/Lang::get\s*\(\s*[\'"]([^\'"]+)[\'"]/'
],
'js_patterns' => [
'/trans\s*\(\s*[\'"]([^\'"]+)[\'"]/',
'/t\s*\(\s*[\'"]([^\'"]+)[\'"]/',
'/\$t\s*\(\s*[\'"]([^\'"]+)[\'"]/',
'/translate\s*\(\s*[\'"]([^\'"]+)[\'"]/',
'/i18n\.t\s*\(\s*[\'"]([^\'"]+)[\'"]/'
],
'html_patterns' => [
'/data-i18n\s*=\s*[\'"]([^\'"]+)[\'"]/',
'/data-translate\s*=\s*[\'"]([^\'"]+)[\'"]/',
'/data-trans\s*=\s*[\'"]([^\'"]+)[\'"]/'
],
'twig_patterns' => [
'/\{\{\s*trans\s*\([\'"]([^\'"]+)[\'"]/',
'/\{\{\s*t\s*\([\'"]([^\'"]+)[\'"]/',
'/\{\{\s*__\s*\([\'"]([^\'"]+)[\'"]/'
],
'vue_patterns' => [
'/\$t\s*\(\s*[\'"]([^\'"]+)[\'"]/',
'/t\s*\(\s*[\'"]([^\'"]+)[\'"]/',
'/v-t\s*=\s*[\'"]([^\'"]+)[\'"]/'
]
];
}
/**
* Get configuration.
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set configuration.
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* Create extractor instance.
*/
public static function create(array $config = []): self
{
return new self($config);
}
/**
* Create for PHP project.
*/
public static function forPhp(): self
{
return new self([
'file_patterns' => ['*.php', '*.twig'],
'exclude_patterns' => ['vendor/*', 'node_modules/*', 'storage/*']
]);
}
/**
* Create for JavaScript project.
*/
public static function forJs(): self
{
return new self([
'file_patterns' => ['*.js', '*.jsx', '*.ts', '*.tsx', '*.vue', '*.html'],
'exclude_patterns' => ['node_modules/*', 'dist/*', 'build/*']
]);
}
/**
* Create for full-stack project.
*/
public static function forFullStack(): self
{
return new self([
'file_patterns' => ['*.php', '*.js', '*.jsx', '*.ts', '*.tsx', '*.vue', '*.html', '*.twig'],
'exclude_patterns' => ['vendor/*', 'node_modules/*', 'storage/*', 'dist/*', 'build/*']
]);
}
}