Files
sam-api/app/Console/Commands/UpdateLogicalRelationships.php
hskwon 1a796462e4 feat: ERD 자동 생성 시스템 구축 및 모델 오류 수정
- GraphViz 설치를 통한 ERD 다이어그램 생성 지원
- BelongsToTenantTrait → BelongsToTenant 트레잇명 수정
- Estimate, EstimateItem 모델의 인터페이스 참조 오류 해결
- 60개 모델의 완전한 관계도 생성 (graph.png, 4.1MB)
- beyondcode/laravel-er-diagram-generator 패키지 활용

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-24 22:30:28 +09:00

219 lines
7.3 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 = [];
// 관계 메서드 패턴 검출
$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*([^,\)]+)/',
];
foreach ($patterns as $type => $pattern) {
if (preg_match_all($pattern, $content, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
$relationships[] = [
'method' => $match[1],
'type' => $type,
'related_model' => trim($match[2], '"\''),
'foreign_key' => null,
'local_key' => null
];
}
}
}
return $relationships;
}
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) {
$relatedTable = (new $rel['related_model'])->getTable();
$content .= "- **{$rel['method']}()**: {$rel['type']} → `{$relatedTable}`";
if ($rel['foreign_key']) {
$content .= " (FK: `{$rel['foreign_key']}`)";
}
$content .= "\n";
}
$content .= "\n";
}
File::put($documentPath, $content);
$this->info("📄 문서 업데이트: {$documentPath}");
}
}