- use 문 파싱 추가하여 짧은 클래스명을 FQCN으로 변환 - self/static 자기 참조 관계 정상 처리 - Polymorphic 관계 지원 (morphTo, morphMany, morphOne) - 클래스 존재 확인 및 안전한 에러 처리 - ::class 문자열 오류 수정 마이그레이션 실행 시 'Class BoardSetting::class not found' 에러 해결
329 lines
12 KiB
PHP
329 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\File;
|
|
use ReflectionClass;
|
|
|
|
class UpdateLogicalRelationships extends Command
|
|
{
|
|
protected $signature = 'db:update-relationships';
|
|
protected $description = '모델에서 논리적 관계를 추출하여 문서 업데이트';
|
|
|
|
public function handle()
|
|
{
|
|
$this->info('🔄 논리적 관계 문서 업데이트 시작...');
|
|
|
|
$relationships = $this->extractModelRelationships();
|
|
$this->updateLogicalDocument($relationships);
|
|
|
|
$this->info('✅ 논리적 관계 문서 업데이트 완료!');
|
|
}
|
|
|
|
private function extractModelRelationships(): array
|
|
{
|
|
$relationships = [];
|
|
$modelPath = app_path('Models');
|
|
|
|
// 모든 모델 파일 스캔
|
|
$modelFiles = File::allFiles($modelPath);
|
|
|
|
foreach ($modelFiles as $file) {
|
|
if ($file->getExtension() !== 'php') continue;
|
|
|
|
$className = $this->getClassNameFromFile($file);
|
|
if (!$className || !class_exists($className)) continue;
|
|
|
|
try {
|
|
$reflection = new ReflectionClass($className);
|
|
|
|
// 모델이 Eloquent Model인지 확인
|
|
if (!$reflection->isSubclassOf(\Illuminate\Database\Eloquent\Model::class)) {
|
|
continue;
|
|
}
|
|
|
|
// Abstract 클래스 건너뛰기
|
|
if ($reflection->isAbstract()) {
|
|
continue;
|
|
}
|
|
|
|
// 테이블 이름 직접 추출
|
|
$tableName = $this->getTableNameFromModel($className, $reflection);
|
|
if (!$tableName) continue;
|
|
|
|
$relationships[$tableName] = [
|
|
'model' => $className,
|
|
'relationships' => $this->getModelRelationshipsFromFile($file, $className)
|
|
];
|
|
|
|
} catch (\Exception $e) {
|
|
$this->warn("모델 분석 실패: {$className} - " . $e->getMessage());
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return $relationships;
|
|
}
|
|
|
|
private function getTableNameFromModel(string $className, ReflectionClass $reflection): ?string
|
|
{
|
|
// 클래스명에서 테이블명 추정
|
|
$modelName = class_basename($className);
|
|
$tableName = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $modelName));
|
|
|
|
// 복수형으로 변환 (간단한 규칙)
|
|
if (!str_ends_with($tableName, 's')) {
|
|
$tableName .= 's';
|
|
}
|
|
|
|
return $tableName;
|
|
}
|
|
|
|
private function getModelRelationshipsFromFile($file, string $className): array
|
|
{
|
|
$content = File::get($file->getRealPath());
|
|
$relationships = [];
|
|
|
|
// use 문 추출
|
|
$useStatements = $this->extractUseStatements($content);
|
|
|
|
// 현재 파일의 namespace 추출
|
|
$currentNamespace = $this->extractNamespace($content);
|
|
|
|
// 관계 메서드 패턴 검출
|
|
$patterns = [
|
|
'belongsTo' => '/public\s+function\s+(\w+)\s*\([^)]*\)\s*[^{]*{\s*return\s+\$this->belongsTo\s*\(\s*([^,\)]+)/',
|
|
'hasMany' => '/public\s+function\s+(\w+)\s*\([^)]*\)\s*[^{]*{\s*return\s+\$this->hasMany\s*\(\s*([^,\)]+)/',
|
|
'hasOne' => '/public\s+function\s+(\w+)\s*\([^)]*\)\s*[^{]*{\s*return\s+\$this->hasOne\s*\(\s*([^,\)]+)/',
|
|
'belongsToMany' => '/public\s+function\s+(\w+)\s*\([^)]*\)\s*[^{]*{\s*return\s+\$this->belongsToMany\s*\(\s*([^,\)]+)/',
|
|
'morphTo' => '/public\s+function\s+(\w+)\s*\([^)]*\)\s*[^{]*{\s*return\s+\$this->morphTo\s*\(/',
|
|
'morphMany' => '/public\s+function\s+(\w+)\s*\([^)]*\)\s*[^{]*{\s*return\s+\$this->morphMany\s*\(\s*([^,\)]+)/',
|
|
'morphOne' => '/public\s+function\s+(\w+)\s*\([^)]*\)\s*[^{]*{\s*return\s+\$this->morphOne\s*\(\s*([^,\)]+)/',
|
|
];
|
|
|
|
foreach ($patterns as $type => $pattern) {
|
|
if (preg_match_all($pattern, $content, $matches, PREG_SET_ORDER)) {
|
|
foreach ($matches as $match) {
|
|
// morphTo는 관련 모델이 없으므로 특별 처리
|
|
if ($type === 'morphTo') {
|
|
$relationships[] = [
|
|
'method' => $match[1],
|
|
'type' => $type,
|
|
'related_model' => '(Polymorphic)',
|
|
'foreign_key' => null,
|
|
'local_key' => null
|
|
];
|
|
continue;
|
|
}
|
|
|
|
// ::class 제거 및 따옴표 제거
|
|
$relatedModel = str_replace('::class', '', trim($match[2], '"\''));
|
|
|
|
// 완전한 클래스명으로 변환
|
|
$fullyQualifiedClass = $this->resolveClassName($relatedModel, $useStatements, $currentNamespace, $className);
|
|
|
|
$relationships[] = [
|
|
'method' => $match[1],
|
|
'type' => $type,
|
|
'related_model' => $fullyQualifiedClass,
|
|
'foreign_key' => null,
|
|
'local_key' => null
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
return $relationships;
|
|
}
|
|
|
|
/**
|
|
* 파일 내용에서 use 문들을 추출
|
|
*/
|
|
private function extractUseStatements(string $content): array
|
|
{
|
|
$useStatements = [];
|
|
|
|
// use 문 패턴: use Full\Namespace\ClassName; 또는 use Full\Namespace\ClassName as Alias;
|
|
if (preg_match_all('/use\s+([^;]+);/', $content, $matches)) {
|
|
foreach ($matches[1] as $useStatement) {
|
|
// as 별칭 처리
|
|
if (strpos($useStatement, ' as ') !== false) {
|
|
[$fullClass, $alias] = array_map('trim', explode(' as ', $useStatement));
|
|
$useStatements[$alias] = $fullClass;
|
|
} else {
|
|
$fullClass = trim($useStatement);
|
|
$shortName = class_basename($fullClass);
|
|
$useStatements[$shortName] = $fullClass;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $useStatements;
|
|
}
|
|
|
|
/**
|
|
* 파일 내용에서 namespace 추출
|
|
*/
|
|
private function extractNamespace(string $content): ?string
|
|
{
|
|
if (preg_match('/namespace\s+([^;]+);/', $content, $matches)) {
|
|
return trim($matches[1]);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 짧은 클래스명을 완전한 클래스명으로 변환
|
|
*/
|
|
private function resolveClassName(string $className, array $useStatements, ?string $currentNamespace, string $currentClassName): string
|
|
{
|
|
// 이미 완전한 클래스명인 경우 (백슬래시 포함)
|
|
if (strpos($className, '\\') !== false) {
|
|
return ltrim($className, '\\');
|
|
}
|
|
|
|
// self/static 처리 - 현재 클래스로 대체
|
|
if ($className === 'self' || $className === 'static') {
|
|
return $currentClassName;
|
|
}
|
|
|
|
// use 문에서 찾기
|
|
if (isset($useStatements[$className])) {
|
|
return $useStatements[$className];
|
|
}
|
|
|
|
// 같은 namespace에 있다고 가정
|
|
if ($currentNamespace) {
|
|
return $currentNamespace . '\\' . $className;
|
|
}
|
|
|
|
// 그 외의 경우 그대로 반환
|
|
return $className;
|
|
}
|
|
|
|
private function getModelRelationships(ReflectionClass $reflection, $model): array
|
|
{
|
|
$relationships = [];
|
|
$methods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC);
|
|
|
|
foreach ($methods as $method) {
|
|
if ($method->getDeclaringClass()->getName() !== $reflection->getName()) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
// 관계 메서드 호출해서 타입 확인
|
|
$result = $method->invoke($model);
|
|
|
|
if ($this->isRelationshipMethod($result)) {
|
|
$relationships[] = [
|
|
'method' => $method->getName(),
|
|
'type' => $this->getRelationshipType($result),
|
|
'related_model' => get_class($result->getRelated()),
|
|
'foreign_key' => $this->getForeignKey($result),
|
|
'local_key' => $this->getLocalKey($result)
|
|
];
|
|
}
|
|
} catch (\Exception $e) {
|
|
// 관계 메서드가 아니거나 호출 실패 시 건너뛰기
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return $relationships;
|
|
}
|
|
|
|
private function isRelationshipMethod($result): bool
|
|
{
|
|
return $result instanceof \Illuminate\Database\Eloquent\Relations\Relation;
|
|
}
|
|
|
|
private function getRelationshipType($relation): string
|
|
{
|
|
$className = get_class($relation);
|
|
return class_basename($className);
|
|
}
|
|
|
|
private function getForeignKey($relation): ?string
|
|
{
|
|
return method_exists($relation, 'getForeignKeyName')
|
|
? $relation->getForeignKeyName()
|
|
: null;
|
|
}
|
|
|
|
private function getLocalKey($relation): ?string
|
|
{
|
|
return method_exists($relation, 'getLocalKeyName')
|
|
? $relation->getLocalKeyName()
|
|
: null;
|
|
}
|
|
|
|
private function getClassNameFromFile($file): ?string
|
|
{
|
|
$content = File::get($file->getRealPath());
|
|
|
|
if (!preg_match('/namespace\s+([^;]+);/', $content, $namespaceMatches)) {
|
|
return null;
|
|
}
|
|
|
|
if (!preg_match('/class\s+(\w+)/', $content, $classMatches)) {
|
|
return null;
|
|
}
|
|
|
|
return $namespaceMatches[1] . '\\' . $classMatches[1];
|
|
}
|
|
|
|
private function updateLogicalDocument(array $relationships): void
|
|
{
|
|
$documentPath = base_path('LOGICAL_RELATIONSHIPS.md');
|
|
$timestamp = now()->format('Y-m-d H:i:s');
|
|
|
|
$content = "# 논리적 데이터베이스 관계 문서\n\n";
|
|
$content .= "> **자동 생성**: {$timestamp}\n";
|
|
$content .= "> **소스**: Eloquent 모델 관계 분석\n\n";
|
|
|
|
$content .= "## 📊 모델별 관계 현황\n\n";
|
|
|
|
foreach ($relationships as $tableName => $info) {
|
|
if (empty($info['relationships'])) continue;
|
|
|
|
$content .= "### {$tableName}\n";
|
|
$content .= "**모델**: `{$info['model']}`\n\n";
|
|
|
|
foreach ($info['relationships'] as $rel) {
|
|
try {
|
|
// Polymorphic 관계는 특별 표시
|
|
if ($rel['related_model'] === '(Polymorphic)') {
|
|
$content .= "- **{$rel['method']}()**: {$rel['type']} → `(Polymorphic)`\n";
|
|
continue;
|
|
}
|
|
|
|
// 관련 모델 클래스가 존재하는지 확인
|
|
if (!class_exists($rel['related_model'])) {
|
|
$this->warn("모델 클래스가 존재하지 않음: {$rel['related_model']}");
|
|
continue;
|
|
}
|
|
|
|
$relatedTable = (new $rel['related_model'])->getTable();
|
|
$content .= "- **{$rel['method']}()**: {$rel['type']} → `{$relatedTable}`";
|
|
|
|
if ($rel['foreign_key']) {
|
|
$content .= " (FK: `{$rel['foreign_key']}`)";
|
|
}
|
|
|
|
$content .= "\n";
|
|
} catch (\Exception $e) {
|
|
$this->warn("관계 처리 실패: {$rel['method']} - " . $e->getMessage());
|
|
continue;
|
|
}
|
|
}
|
|
|
|
$content .= "\n";
|
|
}
|
|
|
|
File::put($documentPath, $content);
|
|
$this->info("📄 문서 업데이트: {$documentPath}");
|
|
}
|
|
} |