| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 3 declare (strict_types=1); 4 namespace WordPress\AiClient\Providers; 5 6 use WordPress\AiClientDependencies\Http\Discovery\Exception\NotFoundException as DiscoveryNotFoundException; 7 use WordPress\AiClient\Common\Exception\InvalidArgumentException; 8 use WordPress\AiClient\Common\Exception\RuntimeException; 9 use WordPress\AiClient\Providers\Contracts\ProviderInterface; 10 use WordPress\AiClient\Providers\Contracts\ProviderWithOperationsHandlerInterface; 11 use WordPress\AiClient\Providers\DTO\ProviderMetadata; 12 use WordPress\AiClient\Providers\DTO\ProviderModelsMetadata; 13 use WordPress\AiClient\Providers\Http\Contracts\HttpTransporterInterface; 14 use WordPress\AiClient\Providers\Http\Contracts\RequestAuthenticationInterface; 15 use WordPress\AiClient\Providers\Http\Contracts\WithHttpTransporterInterface; 16 use WordPress\AiClient\Providers\Http\Contracts\WithRequestAuthenticationInterface; 17 use WordPress\AiClient\Providers\Http\HttpTransporterFactory; 18 use WordPress\AiClient\Providers\Http\Traits\WithHttpTransporterTrait; 19 use WordPress\AiClient\Providers\Models\Contracts\ModelInterface; 20 use WordPress\AiClient\Providers\Models\DTO\ModelConfig; 21 use WordPress\AiClient\Providers\Models\DTO\ModelMetadata; 22 use WordPress\AiClient\Providers\Models\DTO\ModelRequirements; 23 /** 24 * Registry for managing AI providers and their models. 25 * 26 * This class provides a centralized way to register AI providers, discover 27 * their capabilities, and find suitable models based on requirements. 28 * 29 * @since 0.1.0 30 */ 31 class ProviderRegistry implements WithHttpTransporterInterface 32 { 33 use WithHttpTransporterTrait { 34 setHttpTransporter as setHttpTransporterOriginal; 35 } 36 /** 37 * @var array<string, class-string<ProviderInterface>> Mapping of provider IDs to class names. 38 */ 39 private array $registeredIdsToClassNames = []; 40 /** 41 * @var array<class-string<ProviderInterface>, string> Mapping of provider class names to IDs. 42 */ 43 private array $registeredClassNamesToIds = []; 44 /** 45 * @var array<class-string<ProviderInterface>, RequestAuthenticationInterface> Mapping of provider class names to 46 * authentication instances. 47 */ 48 private array $providerAuthenticationInstances = []; 49 /** 50 * Registers a provider class with the registry. 51 * 52 * @since 0.1.0 53 * 54 * @param class-string<ProviderInterface> $className The fully qualified provider class name implementing the 55 * ProviderInterface 56 * @throws InvalidArgumentException If the class doesn't exist or implement the required interface. 57 */ 58 public function registerProvider(string $className): void 59 { 60 if (!class_exists($className)) { 61 throw new InvalidArgumentException(sprintf('Provider class does not exist: %s', $className)); 62 } 63 // Validate that class implements ProviderInterface 64 if (!is_subclass_of($className, ProviderInterface::class)) { 65 throw new InvalidArgumentException(sprintf('Provider class must implement %s: %s', ProviderInterface::class, $className)); 66 } 67 $metadata = $className::metadata(); 68 if (!$metadata instanceof ProviderMetadata) { 69 throw new InvalidArgumentException(sprintf('Provider must return ProviderMetadata from metadata() method: %s', $className)); 70 } 71 // If there is already a HTTP transporter instance set, hook it up to the provider as needed. 72 try { 73 $httpTransporter = $this->getHttpTransporter(); 74 } catch (RuntimeException $e) { 75 /* 76 * If this fails, it's okay. There is no defined sequence between setting the HTTP transporter in the 77 * registry and registering providers in it, so it might be that the transporter is set later. It will be 78 * hooked up then. 79 * But for now we can ignore this exception and attempt to set the default HTTP transporter, if possible. 80 */ 81 try { 82 $this->setHttpTransporter(HttpTransporterFactory::createTransporter()); 83 $httpTransporter = $this->getHttpTransporter(); 84 } catch (DiscoveryNotFoundException $e) { 85 /* 86 * If no HTTP client implementation can be discovered yet, we can ignore this for now. 87 * It might be set later, so it's not a hard error at this point. 88 * We'll try again the next time a provider is registered, or maybe by that time an explicit 89 * HTTP transporter will have been set. 90 */ 91 } 92 } 93 if (isset($httpTransporter)) { 94 $this->setHttpTransporterForProvider($className, $httpTransporter); 95 } 96 // Hook up the request authentication instance, using a default if not set. 97 if (!isset($this->providerAuthenticationInstances[$className])) { 98 $defaultProviderAuthentication = $this->createDefaultProviderRequestAuthentication($className); 99 if ($defaultProviderAuthentication !== null) { 100 $this->providerAuthenticationInstances[$className] = $defaultProviderAuthentication; 101 } 102 } 103 if (isset($this->providerAuthenticationInstances[$className])) { 104 $this->setRequestAuthenticationForProvider($className, $this->providerAuthenticationInstances[$className]); 105 } 106 $this->registeredIdsToClassNames[$metadata->getId()] = $className; 107 $this->registeredClassNamesToIds[$className] = $metadata->getId(); 108 } 109 /** 110 * Gets a list of all registered provider IDs. 111 * 112 * @since 0.1.0 113 * 114 * @return list<string> List of registered provider IDs. 115 */ 116 public function getRegisteredProviderIds(): array 117 { 118 return array_keys($this->registeredIdsToClassNames); 119 } 120 /** 121 * Checks if a provider is registered. 122 * 123 * @since 0.1.0 124 * 125 * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name to check. 126 * @return bool True if the provider is registered. 127 */ 128 public function hasProvider(string $idOrClassName): bool 129 { 130 return $this->isRegisteredId($idOrClassName) || $this->isRegisteredClassName($idOrClassName); 131 } 132 /** 133 * Gets the class name for a registered provider. 134 * 135 * @since 0.1.0 136 * 137 * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name. 138 * @return class-string<ProviderInterface> The provider class name. 139 * @throws InvalidArgumentException If the provider is not registered. 140 */ 141 public function getProviderClassName(string $idOrClassName): string 142 { 143 // If it's already a class name, return it 144 if ($this->isRegisteredClassName($idOrClassName)) { 145 return $idOrClassName; 146 } 147 // If it's a registered ID, return its class name 148 if ($this->isRegisteredId($idOrClassName)) { 149 return $this->registeredIdsToClassNames[$idOrClassName]; 150 } 151 // Not found 152 throw new InvalidArgumentException(sprintf('Provider not registered: %s', $idOrClassName)); 153 } 154 /** 155 * Gets the provider ID for a registered provider. 156 * 157 * @since 0.2.0 158 * 159 * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name. 160 * @return string The provider ID. 161 * @throws InvalidArgumentException If the provider is not registered. 162 */ 163 public function getProviderId(string $idOrClassName): string 164 { 165 // If it's already an ID, return it 166 if ($this->isRegisteredId($idOrClassName)) { 167 return $idOrClassName; 168 } 169 // If it's a registered class name, return its ID 170 if ($this->isRegisteredClassName($idOrClassName)) { 171 return $this->registeredClassNamesToIds[$idOrClassName]; 172 } 173 // Not found 174 throw new InvalidArgumentException(sprintf('Provider not registered: %s', $idOrClassName)); 175 } 176 /** 177 * Checks if a provider is properly configured. 178 * 179 * @since 0.1.0 180 * 181 * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name. 182 * @return bool True if the provider is configured and ready to use. 183 */ 184 public function isProviderConfigured(string $idOrClassName): bool 185 { 186 try { 187 $className = $this->resolveProviderClassName($idOrClassName); 188 // Use static method from ProviderInterface 189 /** @var class-string<ProviderInterface> $className */ 190 $availability = $className::availability(); 191 return $availability->isConfigured(); 192 } catch (InvalidArgumentException $e) { 193 return \false; 194 } 195 } 196 /** 197 * Finds models across all available providers that support the given requirements. 198 * 199 * @since 0.1.0 200 * 201 * @param ModelRequirements $modelRequirements The requirements to match against. 202 * @return list<ProviderModelsMetadata> List of provider models metadata that match requirements. 203 */ 204 public function findModelsMetadataForSupport(ModelRequirements $modelRequirements): array 205 { 206 $results = []; 207 foreach ($this->registeredIdsToClassNames as $providerId => $className) { 208 $providerResults = $this->findProviderModelsMetadataForSupport($providerId, $modelRequirements); 209 if (!empty($providerResults)) { 210 // Use static method from ProviderInterface 211 /** @var class-string<ProviderInterface> $className */ 212 $providerMetadata = $className::metadata(); 213 $results[] = new ProviderModelsMetadata($providerMetadata, $providerResults); 214 } 215 } 216 return $results; 217 } 218 /** 219 * Finds models within a specific available provider that support the given requirements. 220 * 221 * @since 0.1.0 222 * 223 * @param string $idOrClassName The provider ID or class name. 224 * @param ModelRequirements $modelRequirements The requirements to match against. 225 * @return list<ModelMetadata> List of model metadata that match requirements. 226 */ 227 public function findProviderModelsMetadataForSupport(string $idOrClassName, ModelRequirements $modelRequirements): array 228 { 229 $className = $this->resolveProviderClassName($idOrClassName); 230 // If the provider is not configured, there is no way to use it, so it is considered unavailable. 231 if (!$this->isProviderConfigured($className)) { 232 return []; 233 } 234 $modelMetadataDirectory = $className::modelMetadataDirectory(); 235 // Filter models that meet requirements 236 $matchingModels = []; 237 foreach ($modelMetadataDirectory->listModelMetadata() as $modelMetadata) { 238 if ($modelRequirements->areMetBy($modelMetadata)) { 239 $matchingModels[] = $modelMetadata; 240 } 241 } 242 return $matchingModels; 243 } 244 /** 245 * Gets a configured model instance from a provider. 246 * 247 * @since 0.1.0 248 * 249 * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name. 250 * @param string $modelId The model identifier. 251 * @param ModelConfig|null $modelConfig The model configuration. 252 * @return ModelInterface The configured model instance. 253 * @throws InvalidArgumentException If provider or model is not found. 254 */ 255 public function getProviderModel(string $idOrClassName, string $modelId, ?ModelConfig $modelConfig = null): ModelInterface 256 { 257 $className = $this->resolveProviderClassName($idOrClassName); 258 $modelInstance = $className::model($modelId, $modelConfig); 259 $this->bindModelDependencies($modelInstance); 260 return $modelInstance; 261 } 262 /** 263 * Binds dependencies to a model instance. 264 * 265 * This method injects required dependencies such as HTTP transporter 266 * and authentication into model instances that need them. 267 * 268 * @since 0.1.0 269 * 270 * @param ModelInterface $modelInstance The model instance to bind dependencies to. 271 * @return void 272 */ 273 public function bindModelDependencies(ModelInterface $modelInstance): void 274 { 275 $className = $this->resolveProviderClassName($modelInstance->providerMetadata()->getId()); 276 if ($modelInstance instanceof WithHttpTransporterInterface) { 277 $modelInstance->setHttpTransporter($this->getHttpTransporter()); 278 } 279 if ($modelInstance instanceof WithRequestAuthenticationInterface) { 280 $requestAuthentication = $this->getProviderRequestAuthentication($className); 281 if ($requestAuthentication !== null) { 282 $modelInstance->setRequestAuthentication($requestAuthentication); 283 } 284 } 285 } 286 /** 287 * Gets the class name for a registered provider (handles both ID and class name input). 288 * 289 * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name. 290 * @return class-string<ProviderInterface> The provider class name. 291 * @throws InvalidArgumentException If provider is not registered. 292 */ 293 private function resolveProviderClassName(string $idOrClassName): string 294 { 295 // If it's already a class name, return it 296 if ($this->isRegisteredClassName($idOrClassName)) { 297 return $idOrClassName; 298 } 299 // If it's a registered ID, return its class name 300 if ($this->isRegisteredId($idOrClassName)) { 301 return $this->registeredIdsToClassNames[$idOrClassName]; 302 } 303 // Not found 304 throw new InvalidArgumentException(sprintf('Provider not registered: %s', $idOrClassName)); 305 } 306 /** 307 * {@inheritDoc} 308 * 309 * @since 0.1.0 310 */ 311 public function setHttpTransporter(HttpTransporterInterface $httpTransporter): void 312 { 313 $this->setHttpTransporterOriginal($httpTransporter); 314 // Make sure all registered providers have the HTTP transporter hooked up as needed. 315 foreach ($this->registeredIdsToClassNames as $className) { 316 $this->setHttpTransporterForProvider($className, $httpTransporter); 317 } 318 } 319 /** 320 * Sets the request authentication instance for the given provider. 321 * 322 * @since 0.1.0 323 * 324 * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name. 325 * @param RequestAuthenticationInterface $requestAuthentication The request authentication instance. 326 */ 327 public function setProviderRequestAuthentication(string $idOrClassName, RequestAuthenticationInterface $requestAuthentication): void 328 { 329 $className = $this->resolveProviderClassName($idOrClassName); 330 $this->providerAuthenticationInstances[$className] = $requestAuthentication; 331 $this->setRequestAuthenticationForProvider($className, $requestAuthentication); 332 } 333 /** 334 * Gets the request authentication instance for the given provider, if set. 335 * 336 * @since 0.1.0 337 * 338 * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name. 339 * @return ?RequestAuthenticationInterface The request authentication instance, or null if not set. 340 */ 341 public function getProviderRequestAuthentication(string $idOrClassName): ?RequestAuthenticationInterface 342 { 343 $className = $this->resolveProviderClassName($idOrClassName); 344 if (!isset($this->providerAuthenticationInstances[$className])) { 345 return null; 346 } 347 return $this->providerAuthenticationInstances[$className]; 348 } 349 /** 350 * Sets the HTTP transporter for a specific provider, hooking up its class instances. 351 * 352 * @since 0.1.0 353 * 354 * @param class-string<ProviderInterface> $className The provider class name. 355 * @param HttpTransporterInterface $httpTransporter The HTTP transporter instance. 356 */ 357 private function setHttpTransporterForProvider(string $className, HttpTransporterInterface $httpTransporter): void 358 { 359 $availability = $className::availability(); 360 if ($availability instanceof WithHttpTransporterInterface) { 361 $availability->setHttpTransporter($httpTransporter); 362 } 363 $modelMetadataDirectory = $className::modelMetadataDirectory(); 364 if ($modelMetadataDirectory instanceof WithHttpTransporterInterface) { 365 $modelMetadataDirectory->setHttpTransporter($httpTransporter); 366 } 367 if (is_subclass_of($className, ProviderWithOperationsHandlerInterface::class)) { 368 $operationsHandler = $className::operationsHandler(); 369 if ($operationsHandler instanceof WithHttpTransporterInterface) { 370 $operationsHandler->setHttpTransporter($httpTransporter); 371 } 372 } 373 } 374 /** 375 * Sets the request authentication for a specific provider, hooking up its class instances. 376 * 377 * @since 0.1.0 378 * 379 * @param class-string<ProviderInterface> $className The provider class name. 380 * @param RequestAuthenticationInterface $requestAuthentication The authentication instance. 381 * 382 * @throws InvalidArgumentException If the authentication instance is not of the expected type. 383 */ 384 private function setRequestAuthenticationForProvider(string $className, RequestAuthenticationInterface $requestAuthentication): void 385 { 386 $authenticationMethod = $className::metadata()->getAuthenticationMethod(); 387 if ($authenticationMethod === null) { 388 throw new InvalidArgumentException(sprintf('Provider %s does not expect any authentication, but got %s.', $className, get_class($requestAuthentication))); 389 } 390 $expectedClass = $authenticationMethod->getImplementationClass(); 391 if (!$requestAuthentication instanceof $expectedClass) { 392 throw new InvalidArgumentException(sprintf('Provider %s expects authentication of type %s, but got %s.', $className, $expectedClass, get_class($requestAuthentication))); 393 } 394 $availability = $className::availability(); 395 if ($availability instanceof WithRequestAuthenticationInterface) { 396 $availability->setRequestAuthentication($requestAuthentication); 397 } 398 $modelMetadataDirectory = $className::modelMetadataDirectory(); 399 if ($modelMetadataDirectory instanceof WithRequestAuthenticationInterface) { 400 $modelMetadataDirectory->setRequestAuthentication($requestAuthentication); 401 } 402 if (is_subclass_of($className, ProviderWithOperationsHandlerInterface::class)) { 403 $operationsHandler = $className::operationsHandler(); 404 if ($operationsHandler instanceof WithRequestAuthenticationInterface) { 405 $operationsHandler->setRequestAuthentication($requestAuthentication); 406 } 407 } 408 } 409 /** 410 * Creates a default request authentication instance for a provider. 411 * 412 * @since 0.1.0 413 * 414 * @param class-string<ProviderInterface> $className The provider class name. 415 * @return ?RequestAuthenticationInterface The default request authentication instance, or null if not required or 416 * if no credential data can be found. 417 */ 418 private function createDefaultProviderRequestAuthentication(string $className): ?RequestAuthenticationInterface 419 { 420 $providerMetadata = $className::metadata(); 421 $providerId = $providerMetadata->getId(); 422 $authenticationMethod = $providerMetadata->getAuthenticationMethod(); 423 if ($authenticationMethod === null) { 424 return null; 425 } 426 $authenticationClass = $authenticationMethod->getImplementationClass(); 427 if ($authenticationClass === null) { 428 return null; 429 } 430 $authenticationSchema = $authenticationClass::getJsonSchema(); 431 // Iterate over all JSON schema object properties to try to determine the necessary authentication data. 432 $authenticationData = []; 433 if (isset($authenticationSchema['properties']) && is_array($authenticationSchema['properties'])) { 434 /** @var array<string, mixed> $details */ 435 foreach ($authenticationSchema['properties'] as $property => $details) { 436 $envVarName = $this->getEnvVarName($providerId, $property); 437 // Try to get the value from environment variable or constant. 438 $envValue = getenv($envVarName); 439 if ($envValue === \false) { 440 if (!defined($envVarName)) { 441 continue; 442 // Skip if neither environment variable nor constant is defined. 443 } 444 $envValue = constant($envVarName); 445 if (!is_scalar($envValue)) { 446 continue; 447 } 448 } 449 if (isset($details['type'])) { 450 switch ($details['type']) { 451 case 'boolean': 452 $authenticationData[$property] = filter_var($envValue, \FILTER_VALIDATE_BOOLEAN); 453 break; 454 case 'number': 455 $authenticationData[$property] = (int) $envValue; 456 break; 457 case 'string': 458 default: 459 $authenticationData[$property] = (string) $envValue; 460 } 461 } else { 462 // Default to string if no type is specified. 463 $authenticationData[$property] = (string) $envValue; 464 } 465 } 466 // If any required fields are missing, return null to avoid immediate errors. 467 if (isset($authenticationSchema['required']) && is_array($authenticationSchema['required'])) { 468 /** @var list<string> $requiredProperties */ 469 $requiredProperties = $authenticationSchema['required']; 470 if (array_diff_key(array_flip($requiredProperties), $authenticationData)) { 471 return null; 472 } 473 } 474 } 475 /** @var RequestAuthenticationInterface */ 476 /** @var array<string, mixed> $authenticationData */ 477 return $authenticationClass::fromArray($authenticationData); 478 } 479 /** 480 * Checks if the given value is a registered provider class name. 481 * 482 * @since 0.4.0 483 * 484 * @param string $idOrClassName The value to check. 485 * @return bool True if it's a registered class name. 486 * @phpstan-assert-if-true class-string<ProviderInterface> $idOrClassName 487 */ 488 private function isRegisteredClassName(string $idOrClassName): bool 489 { 490 return isset($this->registeredClassNamesToIds[$idOrClassName]); 491 } 492 /** 493 * Checks if the given value is a registered provider ID. 494 * 495 * @since 0.4.0 496 * 497 * @param string $idOrClassName The value to check. 498 * @return bool True if it's a registered provider ID. 499 */ 500 private function isRegisteredId(string $idOrClassName): bool 501 { 502 return isset($this->registeredIdsToClassNames[$idOrClassName]); 503 } 504 /** 505 * Converts a provider ID and field name to a constant case environment variable name. 506 * 507 * @since 0.1.0 508 * 509 * @param string $providerId The provider ID. 510 * @param string $field The field name. 511 * @return string The environment variable name in CONSTANT_CASE. 512 */ 513 private function getEnvVarName(string $providerId, string $field): string 514 { 515 // Convert camelCase or kebab-case or snake_case to CONSTANT_CASE. 516 $constantCaseProviderId = strtoupper((string) preg_replace('/([a-z])([A-Z])/', '$1_$2', str_replace('-', '_', $providerId))); 517 $constantCaseField = strtoupper((string) preg_replace('/([a-z])([A-Z])/', '$1_$2', str_replace('-', '_', $field))); 518 return "{$constantCaseProviderId}_{$constantCaseField}"; 519 } 520 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Sat Jun 13 09:38:55 2026 | Cross-referenced by PHPXref |