Files
Gnuboard7/app/Rules/ValidParentLayout.php
2026-04-01 10:30:52 +09:00

175 lines
5.1 KiB
PHP

<?php
namespace App\Rules;
use App\Models\TemplateLayout;
use Closure;
use Illuminate\Contracts\Validation\DataAwareRule;
use Illuminate\Contracts\Validation\ValidationRule;
class ValidParentLayout implements ValidationRule, DataAwareRule
{
private const MAX_INHERITANCE_DEPTH = 10;
/**
* 검증할 전체 데이터
*/
protected array $data = [];
/**
* 전체 데이터 설정
*/
public function setData(array $data): static
{
$this->data = $data;
return $this;
}
/**
* 부모 레이아웃 검증
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
// extends 필드가 없으면 검증 통과 (상속하지 않는 레이아웃)
if (empty($value)) {
return;
}
// extends 필드는 문자열이어야 함
if (!is_string($value)) {
$fail(__('validation.layout_inheritance.extends_must_be_string'));
return;
}
// template_id 가져오기 (우선순위: data -> 라우트 파라미터)
$templateId = $this->data['template_id'] ?? null;
// data에 없으면 라우트 파라미터에서 templateName으로 조회
if (! $templateId) {
$templateName = request()->route('templateName');
if ($templateName) {
$template = \App\Models\Template::where('identifier', $templateName)->first();
$templateId = $template?->id;
}
}
if (! $templateId) {
$fail(__('validation.request.template_id.required'));
return;
}
// 부모 레이아웃이 존재하는지 확인
$parentLayout = TemplateLayout::where('template_id', $templateId)
->where('name', $value)
->first();
if (!$parentLayout) {
$fail(__('validation.layout_inheritance.parent_not_found', ['parent' => $value]));
return;
}
// 순환 참조 검증
$visited = [];
$currentLayoutName = $this->data['layout_name'] ?? $this->data['name'] ?? null;
if ($currentLayoutName && !$this->checkCircularReference($templateId, $value, $visited, $currentLayoutName)) {
$trace = implode(' → ', $visited).' → '.$value;
$fail(__('validation.layout_inheritance.circular_reference', ['trace' => $trace]));
return;
}
// 상속 깊이 검증
$depth = $this->calculateInheritanceDepth($templateId, $value);
if ($depth > self::MAX_INHERITANCE_DEPTH) {
$fail(__('validation.layout_inheritance.max_depth_exceeded', ['max' => self::MAX_INHERITANCE_DEPTH]));
return;
}
}
/**
* 순환 참조 확인
*/
private function checkCircularReference(int $templateId, string $parentName, array &$visited, string $currentLayoutName): bool
{
// 현재 레이아웃 이름을 부모로 참조하면 순환 참조
if ($parentName === $currentLayoutName) {
$visited[] = $currentLayoutName;
return false;
}
// 이미 방문한 레이아웃이면 순환 참조
if (in_array($parentName, $visited)) {
return false;
}
$visited[] = $parentName;
// 부모 레이아웃의 extends 확인
$parentLayout = TemplateLayout::where('template_id', $templateId)
->where('name', $parentName)
->first();
if (!$parentLayout) {
return true; // 부모가 없으면 순환 참조 아님
}
$parentContent = is_string($parentLayout->content)
? json_decode($parentLayout->content, true)
: $parentLayout->content;
// 부모도 extends가 있으면 재귀 확인
if (isset($parentContent['extends'])) {
return $this->checkCircularReference($templateId, $parentContent['extends'], $visited, $currentLayoutName);
}
return true;
}
/**
* 상속 깊이 계산
*/
private function calculateInheritanceDepth(int $templateId, string $layoutName): int
{
$depth = 1; // 현재 레이아웃부터 시작
$currentName = $layoutName;
$visited = [];
while ($currentName && $depth <= self::MAX_INHERITANCE_DEPTH) {
// 무한 루프 방지
if (in_array($currentName, $visited)) {
return self::MAX_INHERITANCE_DEPTH + 1; // 순환 참조로 최대 깊이 초과 반환
}
$visited[] = $currentName;
$layout = TemplateLayout::where('template_id', $templateId)
->where('name', $currentName)
->first();
if (!$layout) {
break;
}
$content = is_string($layout->content)
? json_decode($layout->content, true)
: $layout->content;
if (isset($content['extends'])) {
$currentName = $content['extends'];
$depth++;
} else {
break;
}
}
return $depth;
}
}