resolved module providers (cached per request) */ private static ?array $moduleProviders = null; /** @var array key→provider lookup */ private static array $moduleProviderByKey = []; public static function tenantScopeFilters(): array { $filters = SearchSqlResourceProvider::tenantScopeFilters(); // Module providers may contribute tenant scope filters via their resource definitions foreach (self::resolveModuleProviders() as $provider) { foreach ($provider->resources() as $key => $resource) { if (isset($resource['tenant_filter']) && $resource['tenant_filter'] !== '') { $filters[$key] = $resource['tenant_filter']; } } } return $filters; } public static function resources(string $query, string $locale): array { return SearchSqlResourceProvider::resources($query, $locale); } /** * Return module-contributed search resource definitions. * These are separate from core SQL resources and processed by SearchDataService. * * @return array */ public static function moduleResources(): array { $resources = []; foreach (self::resolveModuleProviders() as $provider) { foreach ($provider->resources() as $key => $resource) { $resources[$key] = $resource; } } return $resources; } public static function iconForKey(string $key): string { // Check module provider key→icon mapping $moduleProvider = self::$moduleProviderByKey[$key] ?? null; if ($moduleProvider !== null) { $mapped = $moduleProvider->mapResultItem($key, []); if ($mapped !== null && isset($mapped['icon']) && $mapped['icon'] !== '') { return $mapped['icon']; } } return SearchUiMetaProvider::iconForKey($key); } public static function listUrl(string $key, string $query): string { // Ensure module providers are resolved before lookup self::resolveModuleProviders(); $provider = self::$moduleProviderByKey[$key] ?? null; if ($provider !== null) { $url = $provider->listUrl($key, urlencode($query)); if ($url !== '') { return $url; } } return SearchUiMetaProvider::listUrl($key, $query); } public static function mapPreviewItem(string $key, array $data, string $query): ?array { self::resolveModuleProviders(); $provider = self::$moduleProviderByKey[$key] ?? null; if ($provider !== null) { $mapped = $provider->mapResultItem($key, $data); if ($mapped !== null) { return ['label' => $mapped['title'], 'url' => $mapped['url']]; } return null; } return SearchItemMapperProvider::mapPreviewItem($key, $data, $query); } public static function mapResultItem(string $key, string $label, array $data): ?array { self::resolveModuleProviders(); $provider = self::$moduleProviderByKey[$key] ?? null; if ($provider !== null) { $mapped = $provider->mapResultItem($key, $data); if ($mapped !== null) { return [ 'type' => $label, 'title' => $mapped['title'], 'description' => $mapped['subtitle'] ?? '', 'url' => $mapped['url'], 'image' => '', 'icon' => $mapped['icon'] ?? self::iconForKey($key), ]; } return null; } return SearchItemMapperProvider::mapResultItem($key, $label, $data); } /** * Resolve and cache module search providers for the current request. * * @return list */ private static function resolveModuleProviders(): array { if (self::$moduleProviders !== null) { return self::$moduleProviders; } self::$moduleProviders = []; self::$moduleProviderByKey = []; try { /** @var ModuleRegistry $registry */ $registry = app(ModuleRegistry::class); foreach ($registry->getSearchResources() as $providerClass) { if (!class_exists($providerClass)) { continue; } $provider = new $providerClass(); if ($provider instanceof SearchResourceProvider) { self::$moduleProviders[] = $provider; foreach ($provider->resources() as $key => $resource) { self::$moduleProviderByKey[$key] = $provider; } } } } catch (\Throwable) { // fail-open: module registry may not be available } return self::$moduleProviders; } /** * Reset cached module providers (for testing). */ public static function resetModuleProviders(): void { self::$moduleProviders = null; self::$moduleProviderByKey = []; } public static function hotkeyPreviewResource(string $query): ?array { return SearchSpecialResourceProvider::hotkeyPreviewResource($query); } public static function hotkeyResultItems(string $query): array { return SearchSpecialResourceProvider::hotkeyResultItems($query); } public static function docsPreviewResource(string $query): ?array { return SearchSpecialResourceProvider::docsPreviewResource($query); } public static function docsResultItems(string $query): array { return SearchSpecialResourceProvider::docsResultItems($query); } public static function normalizeScoreQuery(string $query): string { return SearchQueryNormalizer::normalizeScoreQuery($query); } /** * @return array{missingIcons: string[], missingListUrls: string[]} */ public static function providerCoverageGaps(): array { $missingIcons = []; $missingListUrls = []; foreach (SearchSqlResourceProvider::resourceKeys() as $key) { if (!SearchUiMetaProvider::hasIconForKey($key)) { $missingIcons[] = $key; } if (!SearchUiMetaProvider::hasListUrlForKey($key)) { $missingListUrls[] = $key; } } return [ 'missingIcons' => $missingIcons, 'missingListUrls' => $missingListUrls, ]; } }