refactor: rename lib/ to core/ for clearer core-module separation
Rename the top-level lib/ directory to core/ so the project root immediately communicates which code is core platform and which lives in modules/. PHP namespaces (MintyPHP\*) are unchanged — only the Composer PSR-4 path mapping moves from lib/ to core/. Module-internal lib/ directories (modules/*/lib/) are untouched. Updated across the full stack: - composer.json PSR-4 mapping - bootstrap entry points (web/index.php, bin/*, tests/bootstrap.php) - tooling configs (phpstan.neon, phpunit.xml, php-cs-fixer) - 26 architecture contract tests - enforcement-policy, guard-catalog, quality-gates - all documentation (CLAUDE.md, README, 25 docs/, .agents/skills/) - bin/qa-extended.sh search paths - .claude/settings.local.json permission paths Workflow: .agents/runs/CORE-LIB-RENAME-001/ (Analyst → Planner → Executor → Code Review (4 findings fixed) → Security Review → Acceptance Test → Finalizer) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
206
core/Service/User/UserTenantContextService.php
Normal file
206
core/Service/User/UserTenantContextService.php
Normal file
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Service\User;
|
||||
|
||||
use MintyPHP\Repository\Tenant\UserTenantRepositoryInterface;
|
||||
use MintyPHP\Repository\User\UserReadRepositoryInterface;
|
||||
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
|
||||
use MintyPHP\Service\Tenant\TenantScopeService;
|
||||
|
||||
class UserTenantContextService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UserReadRepositoryInterface $userReadRepository,
|
||||
private readonly UserWriteRepositoryInterface $userWriteRepository,
|
||||
private readonly UserTenantRepositoryInterface $userTenantRepository,
|
||||
private readonly TenantScopeService $scopeGateway,
|
||||
private readonly UserDirectoryGateway $directoryGateway
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current tenant ID for a user (with fallback to primary tenant)
|
||||
*/
|
||||
public function getCurrentTenantId(int $userId): ?int
|
||||
{
|
||||
$user = $this->userReadRepository->find($userId);
|
||||
if (!$user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$activeTenantIds = $this->scopeGateway->getUserTenantIds($userId);
|
||||
if (!$activeTenantIds) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Priority: last-used tenant → primary tenant → first available.
|
||||
$currentTenantId = (int) ($user['current_tenant_id'] ?? 0);
|
||||
if ($currentTenantId > 0 && in_array($currentTenantId, $activeTenantIds, true)) {
|
||||
return $currentTenantId;
|
||||
}
|
||||
|
||||
$primaryTenantId = (int) ($user['primary_tenant_id'] ?? 0);
|
||||
if ($primaryTenantId > 0 && in_array($primaryTenantId, $activeTenantIds, true)) {
|
||||
return $primaryTenantId;
|
||||
}
|
||||
|
||||
return $activeTenantIds[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current tenant data for a user
|
||||
*/
|
||||
public function getCurrentTenant(int $userId): ?array
|
||||
{
|
||||
$currentTenantId = $this->getCurrentTenantId($userId);
|
||||
if (!$currentTenantId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->directoryGateway->findTenant($currentTenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tenants the user has access to
|
||||
*/
|
||||
public function getAvailableTenants(int $userId): array
|
||||
{
|
||||
$tenantIds = $this->scopeGateway->getUserTenantIds($userId);
|
||||
if (!$tenantIds) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$tenants = [];
|
||||
foreach ($tenantIds as $tenantId) {
|
||||
$tenant = $this->directoryGateway->findTenant((int) $tenantId);
|
||||
if ($tenant && (($tenant['status'] ?? 'active') === 'active')) {
|
||||
$tenants[] = $tenant;
|
||||
}
|
||||
}
|
||||
|
||||
usort($tenants, static function ($a, $b) {
|
||||
return strcmp((string) ($a['description'] ?? ''), (string) ($b['description'] ?? ''));
|
||||
});
|
||||
|
||||
return $tenants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get only explicitly assigned active tenants for a user.
|
||||
* This intentionally ignores global tenant-scope bypass permissions.
|
||||
*/
|
||||
public function getAssignedActiveTenants(int $userId): array
|
||||
{
|
||||
if ($userId <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$tenantIds = array_values(array_unique(array_map(
|
||||
'intval',
|
||||
$this->userTenantRepository->listTenantIdsByUserId($userId)
|
||||
)));
|
||||
$tenantIds = array_values(array_filter($tenantIds, static fn ($id) => $id > 0));
|
||||
if (!$tenantIds) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$activeTenantIds = $this->directoryGateway->listActiveTenantIdsByIds($tenantIds);
|
||||
if (!$activeTenantIds) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$tenantsById = [];
|
||||
foreach ($this->directoryGateway->listTenantsByIds($activeTenantIds) as $tenant) {
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
if ($tenantId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$tenantsById[$tenantId] = $tenant;
|
||||
}
|
||||
|
||||
$tenants = [];
|
||||
foreach ($tenantIds as $tenantId) {
|
||||
if (isset($tenantsById[$tenantId])) {
|
||||
$tenants[] = $tenantsById[$tenantId];
|
||||
}
|
||||
}
|
||||
|
||||
usort($tenants, static function ($a, $b) {
|
||||
return strcmp((string) ($a['description'] ?? ''), (string) ($b['description'] ?? ''));
|
||||
});
|
||||
|
||||
return $tenants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available departments grouped by tenant for a user.
|
||||
* Returns an array of groups: ['tenant' => [...], 'departments' => [...]]
|
||||
*/
|
||||
public function getAvailableDepartmentsByTenant(int $userId): array
|
||||
{
|
||||
$tenants = $this->getAvailableTenants($userId);
|
||||
if (!$tenants) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$groups = [];
|
||||
foreach ($tenants as $tenant) {
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
if ($tenantId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$departments = $this->directoryGateway->listDepartmentsByTenantIds([$tenantId]);
|
||||
usort($departments, static function ($a, $b) {
|
||||
return strcmp((string) ($a['description'] ?? ''), (string) ($b['description'] ?? ''));
|
||||
});
|
||||
$departments = array_map(static function (array $department): array {
|
||||
return [
|
||||
'id' => (int) ($department['id'] ?? 0),
|
||||
'uuid' => $department['uuid'] ?? '',
|
||||
'description' => $department['description'] ?? '',
|
||||
];
|
||||
}, $departments);
|
||||
|
||||
$groups[] = [
|
||||
'tenant' => [
|
||||
'id' => (int) ($tenant['id'] ?? 0),
|
||||
'uuid' => $tenant['uuid'] ?? '',
|
||||
'description' => $tenant['description'] ?? '',
|
||||
],
|
||||
'departments' => $departments,
|
||||
];
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current tenant for a user (with validation)
|
||||
*/
|
||||
public function setCurrentTenant(int $userId, int $tenantId): array
|
||||
{
|
||||
if ($tenantId <= 0) {
|
||||
return ['ok' => false, 'error' => 'invalid_tenant'];
|
||||
}
|
||||
|
||||
$userTenantIds = $this->userTenantRepository->listTenantIdsByUserId($userId);
|
||||
if (!in_array($tenantId, $userTenantIds, true)) {
|
||||
return ['ok' => false, 'error' => 'tenant_not_assigned'];
|
||||
}
|
||||
|
||||
$tenant = $this->directoryGateway->findTenant($tenantId);
|
||||
if (!$tenant) {
|
||||
return ['ok' => false, 'error' => 'tenant_not_found'];
|
||||
}
|
||||
if (($tenant['status'] ?? 'active') !== 'active') {
|
||||
return ['ok' => false, 'error' => 'tenant_inactive'];
|
||||
}
|
||||
|
||||
$updated = $this->userWriteRepository->setCurrentTenant($userId, $tenantId);
|
||||
if (!$updated) {
|
||||
return ['ok' => false, 'error' => 'update_failed'];
|
||||
}
|
||||
|
||||
return ['ok' => true, 'tenant' => $tenant];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user