TenantAtlas/app/Services/Intune/SettingsCatalogDefinitionResolver.php
2025-12-14 20:23:18 +01:00

273 lines
8.9 KiB
PHP

<?php
namespace App\Services\Intune;
use App\Models\SettingsCatalogDefinition;
use App\Services\Graph\GraphClientInterface;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class SettingsCatalogDefinitionResolver
{
private const CACHE_TTL = 60 * 60 * 24 * 30; // 30 days
private const MEMORY_CACHE_PREFIX = 'settings_catalog_def_';
public function __construct(
private GraphClientInterface $graphClient
) {}
/**
* Resolve multiple definition IDs to their metadata.
*
* @param array<string> $definitionIds
* @return array<string, array> Map of definitionId => metadata
*/
public function resolve(array $definitionIds): array
{
if (empty($definitionIds)) {
return [];
}
$definitions = [];
$missingIds = [];
// Step 1: Check memory cache
foreach ($definitionIds as $id) {
$cached = Cache::get(self::MEMORY_CACHE_PREFIX.$id);
if ($cached) {
$definitions[$id] = $cached;
} else {
$missingIds[] = $id;
}
}
if (empty($missingIds)) {
return $definitions;
}
// Step 2: Check database cache
$dbDefinitions = SettingsCatalogDefinition::findByDefinitionIds($missingIds);
foreach ($dbDefinitions as $definitionId => $dbDef) {
$metadata = $this->transformToMetadata($dbDef);
$definitions[$definitionId] = $metadata;
// Cache in memory
Cache::put(
self::MEMORY_CACHE_PREFIX.$definitionId,
$metadata,
now()->addSeconds(self::CACHE_TTL)
);
$missingIds = array_diff($missingIds, [$definitionId]);
}
if (empty($missingIds)) {
return $definitions;
}
// Step 3: Fetch from Graph API
try {
$graphDefinitions = $this->fetchFromGraph($missingIds);
foreach ($graphDefinitions as $definitionId => $metadata) {
// Store in database
$this->storeInDatabase($definitionId, $metadata);
// Cache in memory
Cache::put(
self::MEMORY_CACHE_PREFIX.$definitionId,
$metadata,
now()->addSeconds(self::CACHE_TTL)
);
$definitions[$definitionId] = $metadata;
}
} catch (\Exception $e) {
Log::error('Failed to fetch setting definitions from Graph API', [
'definition_ids' => $missingIds,
'error' => $e->getMessage(),
]);
}
// Step 4: Fallback for still missing definitions
foreach ($missingIds as $id) {
if (! isset($definitions[$id])) {
$fallback = $this->getFallbackMetadata($id);
$definitions[$id] = $fallback;
// Cache fallback in memory too (short TTL since it's not real data)
Cache::put(
self::MEMORY_CACHE_PREFIX.$id,
$fallback,
now()->addMinutes(5) // Shorter TTL for fallbacks
);
}
}
return $definitions;
}
/**
* Resolve a single definition ID.
*/
public function resolveOne(string $definitionId): ?array
{
$result = $this->resolve([$definitionId]);
return $result[$definitionId] ?? null;
}
/**
* Warm cache for definition IDs without returning data.
* Non-blocking: catches and logs errors.
*/
public function warmCache(array $definitionIds): void
{
try {
$this->resolve($definitionIds);
} catch (\Exception $e) {
Log::warning('Failed to warm cache for setting definitions', [
'definition_ids' => $definitionIds,
'error' => $e->getMessage(),
]);
}
}
/**
* Clear cache for a specific definition or all definitions.
*/
public function clearCache(?string $definitionId = null): void
{
if ($definitionId) {
Cache::forget(self::MEMORY_CACHE_PREFIX.$definitionId);
SettingsCatalogDefinition::where('definition_id', $definitionId)->delete();
} else {
// Clear all memory cache (prefix-based)
Cache::flush();
SettingsCatalogDefinition::truncate();
}
}
/**
* Fetch definitions from Graph API.
*
* @param array<string> $definitionIds
* @return array<string, array>
*/
private function fetchFromGraph(array $definitionIds): array
{
$definitions = [];
// Note: Microsoft Graph API does not support "in" operator for $filter.
// We fetch each definition individually.
// Endpoint: /deviceManagement/configurationSettings/{definitionId}
foreach ($definitionIds as $definitionId) {
try {
$response = $this->graphClient->request(
'GET',
"/deviceManagement/configurationSettings/{$definitionId}"
);
if ($response->successful() && isset($response->data)) {
$item = $response->data;
$definitions[$definitionId] = [
'displayName' => $item['displayName'] ?? $this->prettifyDefinitionId($definitionId),
'description' => $item['description'] ?? null,
'helpText' => $item['helpText'] ?? null,
'categoryId' => $item['categoryId'] ?? null,
'uxBehavior' => $item['uxBehavior'] ?? null,
'raw' => $item,
];
}
} catch (\Exception $e) {
Log::warning('Failed to fetch definition from Graph API', [
'definitionId' => $definitionId,
'error' => $e->getMessage(),
]);
// Continue with other definitions
}
}
return $definitions;
}
/**
* Store definition in database.
*/
private function storeInDatabase(string $definitionId, array $metadata): void
{
SettingsCatalogDefinition::updateOrCreate(
['definition_id' => $definitionId],
[
'display_name' => $metadata['displayName'],
'description' => $metadata['description'],
'help_text' => $metadata['helpText'],
'category_id' => $metadata['categoryId'],
'ux_behavior' => $metadata['uxBehavior'],
'raw' => $metadata['raw'],
]
);
}
/**
* Transform database model to metadata array.
*/
private function transformToMetadata(SettingsCatalogDefinition $definition): array
{
return [
'displayName' => $definition->display_name,
'description' => $definition->description,
'helpText' => $definition->help_text,
'categoryId' => $definition->category_id,
'uxBehavior' => $definition->ux_behavior,
'raw' => $definition->raw,
];
}
/**
* Get fallback metadata for unknown definition.
*/
private function getFallbackMetadata(string $definitionId): array
{
return [
'displayName' => $this->prettifyDefinitionId($definitionId),
'description' => null,
'helpText' => null,
'categoryId' => null,
'uxBehavior' => null,
'raw' => null,
'isFallback' => true,
];
}
/**
* Prettify definition ID for fallback display.
* Example: "device_vendor_msft_policy_name" → "Device Vendor Msft Policy Name"
* Special handling for {tenantid} placeholders (Microsoft template definitions).
*/
private function prettifyDefinitionId(string $definitionId): string
{
// Remove {tenantid} placeholder - it's a Microsoft template variable, not part of the name
$cleaned = str_replace(['{tenantid}', '_tenantid_', '_{tenantid}_'], ['', '_', '_'], $definitionId);
// Clean up consecutive underscores
$cleaned = preg_replace('/_+/', '_', $cleaned);
$cleaned = trim($cleaned, '_');
// Convert to title case
$prettified = Str::title(str_replace('_', ' ', $cleaned));
// Remove redundant prefixes to shorten labels
$prettified = preg_replace('/^Device Vendor Msft Passportforwork Policies\s+/', '', $prettified);
$prettified = preg_replace('/^Device Vendor Msft Passportforwork\s+/', 'Windows Hello - ', $prettified);
// Shorten common terms
$prettified = str_replace('Pincomplexity', 'PIN', $prettified);
$prettified = str_replace('Usepassportforwork', 'Enable Windows Hello', $prettified);
return $prettified;
}
}