273 lines
8.9 KiB
PHP
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;
|
|
}
|
|
}
|