Files
Gnuboard7/app/Repositories/LayoutExtensionRepository.php
2026-07-01 10:30:32 +09:00

402 lines
14 KiB
PHP

<?php
namespace App\Repositories;
use App\Contracts\Repositories\LayoutExtensionRepositoryInterface;
use App\Enums\LayoutSourceType;
use App\Models\LayoutExtension;
use Illuminate\Support\Collection;
/**
* 레이아웃 확장 리포지토리 구현
*/
class LayoutExtensionRepository implements LayoutExtensionRepositoryInterface
{
/**
* 특정 확장점에 등록된 확장 목록 조회
*
* @param int $templateId 템플릿 ID
* @param string $extensionPointName 확장점 이름
* @return Collection<int, LayoutExtension>
*/
public function getByExtensionPoint(int $templateId, string $extensionPointName): Collection
{
return LayoutExtension::query()
->where('template_id', $templateId)
->extensionPoints()
->where('target_name', $extensionPointName)
->active()
->ordered()
->get();
}
/**
* 특정 레이아웃을 타겟으로 하는 오버레이 목록 조회
*
* @param int $templateId 템플릿 ID
* @param string $layoutName 레이아웃 이름
* @return Collection<int, LayoutExtension>
*/
public function getOverlaysByLayout(int $templateId, string $layoutName): Collection
{
return LayoutExtension::query()
->where('template_id', $templateId)
->overlays()
->where('target_name', $layoutName)
->active()
->ordered()
->get();
}
/**
* 확장 등록
*
* @param array $data 확장 데이터
* @return LayoutExtension 생성된 확장 모델
*/
public function create(array $data): LayoutExtension
{
return LayoutExtension::create($data);
}
/**
* 확장 등록 또는 업데이트 (upsert)
*
* 동일한 조건의 확장이 존재하면 업데이트하고, 없으면 생성합니다.
* soft delete된 레코드도 복원하여 업데이트합니다.
*
* @param array $attributes 조회 조건
* @param array $values 생성/업데이트할 값
* @return LayoutExtension 생성 또는 업데이트된 확장 모델
*/
public function updateOrCreate(array $attributes, array $values): LayoutExtension
{
// soft delete된 레코드도 포함하여 조회
$existing = LayoutExtension::withTrashed()
->where($attributes)
->first();
if ($existing) {
// 기존 레코드가 있으면 복원 후 업데이트
if ($existing->trashed()) {
$existing->restore();
}
$existing->update($values);
return $existing->fresh();
}
// 없으면 새로 생성
return LayoutExtension::create(array_merge($attributes, $values));
}
/**
* 출처별 확장 삭제 (soft delete)
*
* @param LayoutSourceType $sourceType 출처 타입
* @param string $identifier 출처 식별자
* @return int 삭제된 레코드 수
*/
public function softDeleteBySource(LayoutSourceType $sourceType, string $identifier): int
{
return LayoutExtension::query()
->bySource($sourceType, $identifier)
->delete();
}
/**
* 출처별 확장 복원
*
* @param LayoutSourceType $sourceType 출처 타입
* @param string $identifier 출처 식별자
* @return int 복원된 레코드 수
*/
public function restoreBySource(LayoutSourceType $sourceType, string $identifier): int
{
return LayoutExtension::query()
->withTrashed()
->bySource($sourceType, $identifier)
->restore();
}
/**
* 출처별 확장 영구 삭제
*
* @param LayoutSourceType $sourceType 출처 타입
* @param string $identifier 출처 식별자
* @return int 삭제된 레코드 수
*/
public function forceDeleteBySource(LayoutSourceType $sourceType, string $identifier): int
{
return LayoutExtension::query()
->withTrashed()
->bySource($sourceType, $identifier)
->forceDelete();
}
/**
* 템플릿 오버라이드 확인 (Extension Point용)
*
* @param int $templateId 템플릿 ID
* @param string $extensionPointName 확장점 이름
* @param string $moduleIdentifier 모듈/플러그인 식별자
* @return LayoutExtension|null 템플릿 오버라이드 또는 null
*/
public function findTemplateOverrideForExtensionPoint(
int $templateId,
string $extensionPointName,
string $moduleIdentifier
): ?LayoutExtension {
return LayoutExtension::query()
->where('template_id', $templateId)
->extensionPoints()
->where('target_name', $extensionPointName)
->templateOverrides()
->overridingTarget($moduleIdentifier)
->active()
->first();
}
/**
* 템플릿 오버라이드 확인 (Overlay용)
*
* @param int $templateId 템플릿 ID
* @param string $layoutName 레이아웃 이름
* @param string $moduleIdentifier 모듈/플러그인 식별자
* @return LayoutExtension|null 템플릿 오버라이드 또는 null
*/
public function findTemplateOverrideForOverlay(
int $templateId,
string $layoutName,
string $moduleIdentifier
): ?LayoutExtension {
return LayoutExtension::query()
->where('template_id', $templateId)
->overlays()
->where('target_name', $layoutName)
->templateOverrides()
->overridingTarget($moduleIdentifier)
->active()
->first();
}
/**
* 오버라이드를 고려한 Extension Point 조회
*
* @param int $templateId 템플릿 ID
* @param string $extensionPointName 확장점 이름
* @return Collection<int, LayoutExtension>
*/
public function getResolvedExtensionPoints(int $templateId, string $extensionPointName): Collection
{
// 1. 모든 확장 조회 (모듈/플러그인/템플릿 오버라이드 포함)
$allExtensions = LayoutExtension::query()
->where('template_id', $templateId)
->extensionPoints()
->where('target_name', $extensionPointName)
->active()
->ordered()
->get();
// 2. 템플릿 오버라이드에 가려진 모듈/플러그인 확장 제거
return $this->resolveOverrides($allExtensions);
}
/**
* 오버라이드를 고려한 Overlay 조회
*
* @param int $templateId 템플릿 ID
* @param string $layoutName 레이아웃 이름
* @return Collection<int, LayoutExtension>
*/
public function getResolvedOverlays(int $templateId, string $layoutName): Collection
{
// 1. 모든 오버레이 조회
$allOverlays = LayoutExtension::query()
->where('template_id', $templateId)
->overlays()
->where('target_name', $layoutName)
->active()
->ordered()
->get();
// 2. 템플릿 오버라이드에 가려진 모듈/플러그인 확장 제거
return $this->resolveOverrides($allOverlays);
}
/**
* 템플릿 오버라이드 해석을 적용해 가려진 확장을 제거합니다.
*
* 동일한 (extension_type, target_name) 범위 안에서, 템플릿이 특정
* 모듈/플러그인을 오버라이드하면 그 모듈/플러그인의 원본 확장은
* 화면에 적용되지 않으므로 결과에서 제외합니다. 템플릿 오버라이드 행은
* 항상 유지됩니다.
*
* 렌더링 경로(getResolvedExtensionPoints/getResolvedOverlays)와
* 편집 화면 트리(getByTemplateId)가 동일한 가시성 기준을 공유하도록
* 이 메서드를 단일 출처(SSoT)로 사용합니다.
*
* 호출 측은 동일한 (extension_type, target_name) 범위의 확장만
* 전달해야 합니다. 혼합 컬렉션은 target_name 별로 그룹핑한 뒤
* 그룹마다 호출하십시오.
*
* @param Collection<int, LayoutExtension> $extensions 동일 범위 확장 컬렉션
* @return Collection<int, LayoutExtension> 오버라이드에 가려진 행을 제외한 컬렉션
*/
public function resolveOverrides(Collection $extensions): Collection
{
// 템플릿 오버라이드가 가리키는 모듈/플러그인 식별자 추출
$overriddenSources = $extensions
->where('source_type', LayoutSourceType::Template)
->pluck('override_target')
->filter()
->unique()
->toArray();
return $extensions->filter(function ($extension) use ($overriddenSources) {
// 템플릿 오버라이드는 항상 포함
if ($extension->source_type === LayoutSourceType::Template) {
return true;
}
// 오버라이드된 모듈/플러그인 확장은 제외
return ! in_array($extension->source_identifier, $overriddenSources, true);
})->values();
}
/**
* 특정 템플릿의 모든 확장 조회
*
* @param int $templateId 템플릿 ID
* @return Collection<int, LayoutExtension>
*/
public function getByTemplateId(int $templateId): Collection
{
return LayoutExtension::query()
->where('template_id', $templateId)
->active()
->ordered()
->get();
}
/**
* 특정 템플릿의 확장을 오버라이드 해석을 적용해 조회합니다.
*
* getByTemplateId 가 모든 행을 반환하는 것과 달리, 템플릿 오버라이드에
* 가려져 화면에 적용되지 않는 모듈/플러그인 확장을 제외합니다.
* 레이아웃 편집 화면 좌측 트리가 "실제 화면에 반영되는 확장"만 보여주도록
* 렌더링 경로와 동일한 가시성 기준(resolveOverrides)을 적용합니다.
*
* @param int $templateId 템플릿 ID
* @return Collection<int, LayoutExtension> 오버라이드에 가려진 행을 제외한 컬렉션
*/
public function getResolvedByTemplateId(int $templateId): Collection
{
$all = $this->getByTemplateId($templateId);
// (extension_type, target_name) 별로 그룹핑한 뒤 그룹마다 오버라이드 해석
return $all
->groupBy(fn ($extension) => $extension->extension_type->value.'|'.$extension->target_name)
->flatMap(fn (Collection $group) => $this->resolveOverrides($group))
->values();
}
/**
* ID로 단일 확장 조회
*
* @param int $extensionId 확장 ID
* @return LayoutExtension|null 확장 모델 또는 null
*/
public function findById(int $extensionId): ?LayoutExtension
{
return LayoutExtension::find($extensionId);
}
/**
* 특정 출처(모듈/플러그인)의 모든 확장 조회
*
* @param LayoutSourceType $sourceType 출처 타입
* @param string $identifier 출처 식별자
* @return Collection<int, LayoutExtension>
*/
public function getBySource(LayoutSourceType $sourceType, string $identifier): Collection
{
return LayoutExtension::query()
->bySource($sourceType, $identifier)
->get();
}
/**
* 조회 조건에 일치하는 확장을 조회합니다.
*
* @param array $attributes 조회 조건
* @return LayoutExtension|null 일치하는 확장 또는 null
*/
public function findByAttributes(array $attributes): ?LayoutExtension
{
return LayoutExtension::query()->where($attributes)->first();
}
/**
* 조회 조건에 일치하는 모든 LIVE 확장을 템플릿 구분 없이 조회합니다.
*
* cross-template 판정에 사용 — 동일 확장(source/type/target)이 어느 템플릿들에
* 등록돼 있는지 전수 확인하기 위함. template_id 는 조건에 넣지 않는다.
*
* @param array $attributes 조회 조건 (template_id 제외)
* @return \Illuminate\Database\Eloquent\Collection<int, LayoutExtension>
*/
public function getAllByAttributesAcrossTemplates(array $attributes): \Illuminate\Database\Eloquent\Collection
{
return LayoutExtension::query()->where($attributes)->get();
}
/**
* 확장 업데이트
*
* @param int $extensionId 확장 ID
* @param array $data 업데이트할 데이터
* @return LayoutExtension 업데이트된 확장 모델
*/
public function update(int $extensionId, array $data): LayoutExtension
{
$extension = LayoutExtension::findOrFail($extensionId);
$extension->update($data);
return $extension->fresh();
}
/**
* 확장 content + lock_version 동시 갱신 (낙관적 잠금)
*
* Service 가 호출 직전에 expected_lock_version 검증을 마친 상태로,
* 본 메서드는 update 데이터에 lock_version 증가를 함께 반영한다.
*
* @param int $extensionId 확장 ID
* @param array $data 업데이트 데이터 (content, priority 등)
* @param int $newLockVersion 새 lock_version 값 (currentVersion + 1)
* @return LayoutExtension 업데이트된 확장 모델
*/
public function updateWithLock(int $extensionId, array $data, int $newLockVersion): LayoutExtension
{
$extension = LayoutExtension::findOrFail($extensionId);
$extension->update([...$data, 'lock_version' => $newLockVersion]);
return $extension->fresh();
}
/**
* 특정 템플릿의 모든 확장 삭제
*
* @param int $templateId 템플릿 ID
* @return int 삭제된 레코드 수
*/
public function deleteByTemplateId(int $templateId): int
{
return LayoutExtension::query()
->withTrashed()
->where('template_id', $templateId)
->forceDelete();
}
}