collection = $collection; $this->providerSettings = $this->loadProviderSettings(); $this->providerPlanContext = $providerPlanContext; } public function register(ProviderInterface $provider): void { $providerKey = $this->providerKey($provider->getName()); if (isset($this->registeredNames[$providerKey])) { return; } // Runtime availability belongs to provider discovery/bootstrap, not to // the Core registry. This keeps the registry open to independently // installed provider plugins that are unknown to RuntimeProviderCatalog. if (! $this->isProviderEnabled($provider->getName())) { return; } // Registration is the final runtime gate. Optional provider packages // may expose isExecutable()/isConfigured() without requiring a name // switch in Core; providers that need credentials are not routed until // those credentials are actually present. if (! $this->isProviderExecutable($provider)) { return; } $this->registeredNames[$providerKey] = true; $this->collection->add($provider); } public function getProvidersByPriority(): array { $providers = $this->collection->getByPriority(); if ($this->providerPlanContext !== null && $this->providerPlanContext->hasActivePlan()) { return $this->applyExecutionPlan($providers); } return $this->sortByRuntimeOrder($providers); } private function sortByRuntimeOrder(array $providers): array { usort($providers, function (ProviderInterface $left, ProviderInterface $right): int { $leftOrder = $this->resolveProviderOrder($left); $rightOrder = $this->resolveProviderOrder($right); if ($leftOrder === $rightOrder) { return $right->getPriority() <=> $left->getPriority(); } return $leftOrder <=> $rightOrder; }); return $providers; } private function applyExecutionPlan(array $providers): array { $plannedNames = array_map(fn (string $name): string => $this->providerKey($name), $this->providerPlanContext->getExecutableProviderNames()); $providersByName = []; foreach ($providers as $provider) { $providersByName[$this->providerKey($provider->getName())] = $provider; } $ordered = []; foreach ($plannedNames as $name) { if (isset($providersByName[$name])) { $ordered[] = $providersByName[$name]; } } return $ordered; } public function getProvidersByPriorityForMedia(string $mediaType): array { $normalizedMediaType = MediaType::normalize($mediaType); // AUTO owns cross-domain fan-out. A diagnostic planning context may // contain a single inferred domain, but it must not reduce the set of // registered providers before UniversalSearchService applies each // concrete domain chain. if ( ($normalizedMediaType === MediaType::AUTO || $normalizedMediaType === MediaType::UNKNOWN) && $this->providerPlanContext !== null && $this->providerPlanContext->hasActivePlan() ) { return $this->sortByRuntimeOrder($this->collection->getByPriority()); } if ($this->providerPlanContext !== null && $this->providerPlanContext->hasActivePlan()) { return $this->getProvidersByPriority(); } // AUTO/UNKNOWN is orchestration scope: all registered providers remain // candidates and the orchestrator fans out into concrete domains. if ($normalizedMediaType === MediaType::AUTO || $normalizedMediaType === MediaType::UNKNOWN) { return $this->getProvidersByPriority(); } return array_values(array_filter( $this->getProvidersByPriority(), static function (ProviderInterface $provider) use ($normalizedMediaType): bool { // Concrete-domain routing is capability-driven. A provider plugin // that wants to participate must explicitly declare its domains; // the Core no longer guesses BOOK for unknown/legacy providers. return $provider instanceof ProviderMediaCapabilityInterface && $provider->supportsMediaType($normalizedMediaType); } )); } /** * AUTO orchestration escape hatch: return runtime-registered providers * without applying a single-domain diagnostic execution plan. */ public function getProvidersByPriorityIgnoringPlanForMedia(string $mediaType): array { $providers = $this->sortByRuntimeOrder($this->collection->getByPriority()); $normalizedMediaType = MediaType::normalize($mediaType); if ($normalizedMediaType === MediaType::AUTO || $normalizedMediaType === MediaType::UNKNOWN) { return $providers; } return array_values(array_filter( $providers, static fn (ProviderInterface $provider): bool => $provider instanceof ProviderMediaCapabilityInterface && $provider->supportsMediaType($normalizedMediaType) )); } private function loadProviderSettings(): array { if (! function_exists('get_option')) { return []; } $settings = get_option('tfsi_settings', []); if (! is_array($settings)) { return []; } $providers = $settings['providers'] ?? []; return (new ProviderConfigurationSchema())->normalize($providers); } private function isProviderEnabled(string $providerName): bool { $config = $this->providerConfig($providerName); if (! is_array($config)) { // Unknown providers are allowed by default. Their own bootstrap / // descriptor is responsible for runtime availability; this is what // allows an independently installed provider plugin to register. return true; } if (! array_key_exists('enabled', $config)) { return true; } return (bool) $config['enabled']; } private function isProviderExecutable(ProviderInterface $provider): bool { if (method_exists($provider, 'isExecutable')) { return (bool) $provider->isExecutable(); } if (method_exists($provider, 'isConfigured')) { return (bool) $provider->isConfigured(); } return true; } private function resolveProviderOrder(ProviderInterface $provider): int { $config = $this->providerConfig($provider->getName()); if (is_array($config) && isset($config['order'])) { return max(1, (int) $config['order']); } return max(1, 10000 - (int) $provider->getPriority()); } private function providerConfig(string $providerName): ?array { foreach ($this->providerSettings as $name => $config) { if (is_string($name) && $this->providerKey($name) === $this->providerKey($providerName) && is_array($config)) { return $config; } } return null; } public function providerKey(string $name): string { return strtolower((string) preg_replace('/[^a-z0-9]+/i', '', trim($name))); } public function getRegisteredProviderNames(): array { return array_map( static fn (ProviderInterface $provider): string => $provider->getName(), $this->collection->getByPriority() ); } }