*/ private array $providersByKey; /** * Construct the UseCase with an explicit provider registry. * * Validation: * * - Empty registry is allowed. Phase 6's minimum bootstrap * ships with no providers wired by default; the §10.3 * endpoint must answer `items: []` rather than failing * loudly so the editor surface remains operational while * the admin completes provider setup. * * - Every key MUST be a non-empty string after trimming. * Non-string keys, empty-string keys, and whitespace-only * keys raise {@see InvalidArgumentException}. * * - Every value MUST implement * {@see ProviderInterface}. Other types raise * {@see InvalidArgumentException}. * * @param array $providersByKey Provider registry. * * @throws InvalidArgumentException When any registry entry * violates the validation * rules summarized above. */ public function __construct( array $providersByKey ) { $normalized = array(); foreach ( $providersByKey as $key => $provider ) { if ( ! is_string( $key ) ) { $message = sprintf( 'ListAvailableProvidersUseCase: provider registry keys must be strings; got %s.', gettype( $key ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } if ( trim( $key ) === '' ) { $message = 'ListAvailableProvidersUseCase: provider registry keys must be non-empty strings.'; // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } if ( ! $provider instanceof ProviderInterface ) { $message = sprintf( 'ListAvailableProvidersUseCase: provider registry["%s"] must implement ProviderInterface; got %s.', $key, is_object( $provider ) ? get_class( $provider ) : gettype( $provider ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } $normalized[ $key ] = $provider; } $this->providersByKey = $normalized; } /** * Execute the §10.3 enumeration. * * Iterates the registered providers in registration order and * projects each one onto the §10.3 response shape. See the * file-level docblock for the per-flag derivation rules and * the defensive `try` / `catch` posture around * {@see ProviderInterface::healthCheck()}. * * @return list */ public function execute(): array { $items = array(); foreach ( $this->providersByKey as $registryKey => $provider ) { $items[] = $this->describeProvider( $registryKey, $provider ); } return $items; } /** * Project a single registered provider onto the §10.3 row * shape, applying the defensive `try` / `catch` posture * documented at the file level. * * Identity (`key`, `label`) is read from the provider's own * accessors on the happy path; if any of those accessors — * along with `healthCheck()` and `supportsFeature()` — throws, * the row falls back to the registry key for `key`, a derived * label for `label`, and `false` for all three boolean flags. * * @param string $registryKey Registry key for the provider. * @param ProviderInterface $provider Registered provider instance. * * @return array{key: string, label: string, enabled: bool, configured: bool, search_available: bool} */ private function describeProvider( string $registryKey, ProviderInterface $provider ): array { try { $key = $provider->key(); $label = $provider->label(); $health = $provider->healthCheck(); $enabled = $health->isHealthy(); $configured = $health->status() !== ProviderHealthStatus::STATUS_UNCONFIGURED; $searchAvailable = $provider->supportsFeature( ProviderCapabilities::FEATURE_SEARCH ); return array( 'key' => $key, 'label' => $label, 'enabled' => $enabled, 'configured' => $configured, 'search_available' => $searchAvailable, ); } catch ( Throwable $unused ) { return array( 'key' => $registryKey, 'label' => $registryKey, 'enabled' => false, 'configured' => false, 'search_available' => false, ); } } }