Files
Gnuboard7/app/Services/PermissionService.php
T
2026-07-01 10:30:32 +09:00

294 lines
10 KiB
PHP

<?php
namespace App\Services;
use App\Contracts\Repositories\PermissionRepositoryInterface;
use App\Enums\ExtensionOwnerType;
use App\Enums\PermissionType;
use App\Extension\HookManager;
use App\Models\Permission;
use Illuminate\Database\Eloquent\Collection;
class PermissionService
{
public function __construct(
private PermissionRepositoryInterface $permissionRepository
) {}
/**
* 계층형 권한 트리를 타입별로 그룹화하여 반환합니다.
*
* 모든 레벨이 펼치기/접기 가능한 구조이며, 코어 권한이 항상 먼저 표시됩니다.
* 권한은 type 필드(admin/user)에 따라 그룹화되어 반환됩니다.
*
* @return array 타입별 권한 트리
* [
* 'permissions' => ['admin' => [...], 'user' => [...]],
* 'types' => ['admin', 'user'],
* 'default_type' => 'admin',
* ]
*/
public function getPermissionTree(): array
{
// 최상위 권한(모듈)부터 재귀적으로 자식 로드
$rootPermissions = $this->permissionRepository->getRootsWithChildren();
// 코어 권한을 항상 먼저 표시하기 위해 PHP에서 정렬
$rootPermissions = $rootPermissions->sortBy(function ($permission) {
// 코어는 0, 나머지는 1로 우선순위 부여 후 order로 정렬
$priority = $permission->extension_type === ExtensionOwnerType::Core ? 0 : 1;
return [$priority, $permission->order];
})->values();
// 전체 트리를 먼저 빌드
$fullTree = [];
foreach ($rootPermissions as $permission) {
$fullTree[] = $this->formatPermissionNode($permission);
}
// 타입별로 트리를 재귀적으로 필터링하여 그룹화
$groupedByType = [];
$types = PermissionType::values();
foreach ($types as $type) {
$groupedByType[$type] = [
'label' => PermissionType::from($type)->label(),
'icon' => PermissionType::from($type)->icon(),
'permissions' => $this->filterTreeByType($fullTree, $type),
];
}
$result = [
'permissions' => $groupedByType,
'types' => $types,
'default_type' => PermissionType::Admin->value,
'scope_options' => [
['value' => null, 'label' => __('auth.scope_type_all')],
['value' => 'role', 'label' => __('auth.scope_type_role')],
['value' => 'self', 'label' => __('auth.scope_type_self')],
],
];
// 필터 훅 - 결과 데이터 변형 (확장성 확보)
$result = HookManager::applyFilters('core.permission.filter_tree_result', $result);
return $result;
}
/**
* 단일 권한 노드를 포맷팅합니다 (재귀).
*
* @param Permission $permission 권한 모델
* @return array 포맷된 권한 노드
*/
private function formatPermissionNode(Permission $permission): array
{
$node = [
'id' => $permission->id,
'identifier' => $permission->identifier,
'name' => $permission->getLocalizedName(),
'name_raw' => $permission->name,
'description' => $permission->getLocalizedDescription(),
'description_raw' => $permission->description,
'extension_type' => $permission->extension_type?->value,
'extension_identifier' => $permission->extension_identifier,
'type' => $permission->type?->value,
'is_assignable' => $permission->children->isEmpty(),
'resource_route_key' => $permission->resource_route_key,
'owner_key' => $permission->owner_key,
'children' => [],
'leaf_count' => $this->countLeafNodes($permission),
];
if ($permission->children->isNotEmpty()) {
foreach ($permission->children as $child) {
$node['children'][] = $this->formatPermissionNode($child);
}
}
return $node;
}
/**
* 하위 리프 노드 개수를 계산합니다.
*
* @param Permission $permission 권한 모델
* @return int 리프 노드 개수
*/
private function countLeafNodes(Permission $permission): int
{
if ($permission->children->isEmpty()) {
return 1;
}
$count = 0;
foreach ($permission->children as $child) {
$count += $this->countLeafNodes($child);
}
return $count;
}
/**
* 트리 노드 배열을 특정 타입의 리프 노드만 포함하도록 재귀적으로 필터링합니다.
*
* 리프 노드는 해당 타입과 일치할 때만 포함되며,
* 비리프 노드(그룹)는 하위에 해당 타입의 리프가 존재할 때만 포함됩니다.
*
* @param array $nodes 포맷된 권한 노드 배열
* @param string $type 필터링할 권한 타입 (admin/user)
* @return array 필터링된 노드 배열
*/
private function filterTreeByType(array $nodes, string $type): array
{
$filtered = [];
foreach ($nodes as $node) {
$filteredNode = $this->filterNodeByType($node, $type);
if ($filteredNode !== null) {
$filtered[] = $filteredNode;
}
}
return $filtered;
}
/**
* 단일 노드를 특정 타입으로 필터링합니다 (재귀).
*
* @param array $node 포맷된 권한 노드
* @param string $type 필터링할 권한 타입
* @return array|null 필터링된 노드 또는 null (해당 타입 없음)
*/
private function filterNodeByType(array $node, string $type): ?array
{
// 리프 노드: 타입이 일치하면 포함, 아니면 제외
if (empty($node['children'])) {
return ($node['type'] === $type) ? $node : null;
}
// 비리프 노드: 자식을 재귀적으로 필터링
$filteredChildren = $this->filterTreeByType($node['children'], $type);
// 해당 타입의 하위 노드가 없으면 이 브랜치 제외
if (empty($filteredChildren)) {
return null;
}
$node['children'] = $filteredChildren;
$node['leaf_count'] = $this->countLeafNodesFromArray($filteredChildren);
return $node;
}
/**
* 포맷된 노드 배열에서 리프 노드 개수를 계산합니다.
*
* @param array $nodes 포맷된 권한 노드 배열
* @return int 리프 노드 개수
*/
private function countLeafNodesFromArray(array $nodes): int
{
$count = 0;
foreach ($nodes as $node) {
if (empty($node['children'])) {
$count++;
} else {
$count += $this->countLeafNodesFromArray($node['children']);
}
}
return $count;
}
/**
* 할당 가능한 권한 ID 목록을 반환합니다. (리프 노드만)
*
* @return array 할당 가능한 권한 ID 배열
*/
public function getAssignablePermissionIds(): array
{
return $this->permissionRepository->getAssignableIds();
}
/**
* 모든 권한 목록을 반환합니다.
*
* @return Collection 권한 컬렉션
*/
public function getAllPermissions(): Collection
{
return $this->permissionRepository->getAll();
}
/**
* 레이아웃 편집기 표시 권한 후보 목록을 반환합니다.
*
* 코어 + 활성 확장 권한 전체를 `{key, name}` 형태로 평탄화합니다. `key` 는 권한
* 식별자(예: `core.users.read`), `name` 은 지정 로케일 친화 표시명(다국어 array
* 의 해당 로케일 → 첫 값 → 식별자 순 폴백)입니다. 편집기 속성 모달의 표시 권한
* TagInput 후보로 사용됩니다.
*
* 자식을 가진 상위(카테고리) 권한은 실제 할당 불가하므로 제외하고, 리프 노드
* (`isAssignable` — 자식 없음)만 후보로 반환합니다. 카테고리 행이 후보에 섞여
* 표시되던 결함을 차단합니다.
*
* @param string $locale 표시명 로케일
* @return array<int, array{key: string, name: string}> 권한 후보 목록 (리프 권한)
*/
public function getPermissionCandidates(string $locale): array
{
$all = $this->permissionRepository->getAll();
// 다른 권한의 parent_id 로 참조되는 ID 집합 = 카테고리(자식 보유). N+1 회피를 위해
// 메모리에서 1회 계산한다(getAll 로 이미 전량 로드됨).
$parentIds = $all
->pluck('parent_id')
->filter(fn ($id): bool => $id !== null)
->unique()
->flip();
return $all
->reject(fn (Permission $permission): bool => $parentIds->has($permission->id))
->map(function (Permission $permission) use ($locale): array {
$name = $permission->name;
$label = is_array($name)
? ($name[$locale] ?? (reset($name) ?: $permission->identifier))
: (is_string($name) && $name !== '' ? $name : $permission->identifier);
return [
'key' => (string) $permission->identifier,
'name' => (string) $label,
];
})
->filter(fn (array $row): bool => $row['key'] !== '')
->values()
->all();
}
/**
* 특정 권한을 ID로 조회합니다.
*
* @param int $id 권한 ID
* @return Permission|null 권한 모델 또는 null
*/
public function findById(int $id): ?Permission
{
return $this->permissionRepository->findById($id);
}
/**
* 특정 확장의 권한들을 조회합니다.
*
* @param ExtensionOwnerType $type 확장 타입
* @param string|null $identifier 확장 식별자
* @return Collection 권한 컬렉션
*/
public function getByExtension(ExtensionOwnerType $type, ?string $identifier = null): Collection
{
return $this->permissionRepository->getByExtension($type, $identifier);
}
}