Skip to content
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
3c56f68
Implement AiProviderRegistry with comprehensive test suite
Aug 5, 2025
4b09a07
Fix PHPStan type issues in AiProviderRegistry
Aug 5, 2025
a84a4ac
Remove trailing whitespace from AiProviderRegistry
Aug 5, 2025
40b926b
Integrate AiProviderRegistry with Provider interfaces from PR #35
Aug 7, 2025
7bdcb9c
Fix PHPCS style violations
Aug 7, 2025
877fc55
Fix PHPCS style after rebase
Aug 7, 2025
cacb2dc
Fix tokenCount references in test files after rebase
Aug 7, 2025
63fe203
Fix PHPCS line length warnings in test files
Aug 7, 2025
b1e63d5
Fix duplicate meetsRequirements() method after rebase
Aug 8, 2025
37d1124
Resolve merge conflict in ModelConfigTest
Aug 12, 2025
83927be
Clean up PR #38: Revert unrelated changes per Felix's feedback
Aug 14, 2025
d77b021
Fix MockModel method signature to match trunk ModelInterface
Aug 14, 2025
06411b9
refactor: renamed to ProviderRegistry
JasonTheAdams Aug 14, 2025
b44f26c
fix: removes unused imports
JasonTheAdams Aug 14, 2025
cbf1aef
Merge remote-tracking branch 'origin/trunk' into feature/provider-reg…
Aug 15, 2025
7956769
Merge remote-tracking branch 'fork/feature/provider-registry' into fe…
Aug 15, 2025
4ac777e
fix: not needed because it's used in the parameter
Aug 15, 2025
57dfe25
refractor: type fixing
Aug 15, 2025
8f96b26
refractor : type fix
Aug 15, 2025
766fcf8
refractor : type fix
Aug 15, 2025
8a64e8a
refractor : type fix
Aug 15, 2025
23a2a01
refractor : clean not needed
Aug 15, 2025
14e650b
refractor : type fix
Aug 15, 2025
2aeac0e
fix : Validate that class implements ProviderInterface
Aug 15, 2025
212bab6
refractor: Add proper PHPStan annotations for static method calls on …
Aug 15, 2025
4258145
refractor: type fix
Aug 15, 2025
da98ac1
refractor: type fix
Aug 15, 2025
8de52d2
refractor: type fix
Aug 15, 2025
2f1d300
refactor: reorganize test structure and rename ProviderRegistry test
Aug 15, 2025
db59e19
fix: line length to match phpcs
Aug 15, 2025
2c9ff36
Fix import sorting in ProviderRegistryTest
Aug 15, 2025
03310ba
fix: type
Aug 15, 2025
489a4ed
refactor: remove redundant PHPStan annotation
Aug 15, 2025
cafc630
Fix PHPCS style violations
Aug 7, 2025
df20ba4
Fix PHPCS line length warnings in test files
Aug 7, 2025
c52f1d9
test: fixes broken tests from rebase
JasonTheAdams Aug 15, 2025
3abe579
test: reverts tests back to trunk affected by rebase
JasonTheAdams Aug 15, 2025
45de1fa
test: adds missing EOL
JasonTheAdams Aug 15, 2025
6e25240
Optimize ProviderRegistry::hasProvider() performance by replacing in_…
Aug 15, 2025
f868e7b
Remove redundant interface validation in resolveProviderClassName
Aug 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 231 additions & 0 deletions src/Providers/ProviderRegistry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
<?php

declare(strict_types=1);

namespace WordPress\AiClient\Providers;

use InvalidArgumentException;
use WordPress\AiClient\Providers\Contracts\ProviderInterface;
use WordPress\AiClient\Providers\DTO\ProviderMetadata;
use WordPress\AiClient\Providers\DTO\ProviderModelsMetadata;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
use WordPress\AiClient\Providers\Models\DTO\ModelRequirements;

/**
* Registry for managing AI providers and their models.
*
* This class provides a centralized way to register AI providers, discover
* their capabilities, and find suitable models based on requirements.
*
* @since n.e.x.t
*/
class ProviderRegistry
{
/**
* @var array<string, class-string<ProviderInterface>> Mapping of provider IDs to class names.
*/
private array $providerClassNames = [];


/**
* Registers a provider class with the registry.
*
* @since n.e.x.t
*
* @param class-string<ProviderInterface> $className The fully qualified provider class name implementing the
* ProviderInterface
* @throws InvalidArgumentException If the class doesn't exist or implement the required interface.
*/
public function registerProvider(string $className): void
{
if (!class_exists($className)) {
throw new InvalidArgumentException(
sprintf('Provider class does not exist: %s', $className)
);
}

// Validate that class implements ProviderInterface
if (!is_subclass_of($className, ProviderInterface::class)) {
throw new InvalidArgumentException(
sprintf('Provider class must implement %s: %s', ProviderInterface::class, $className)
);
}

$metadata = $className::metadata();

if (!$metadata instanceof ProviderMetadata) {
throw new InvalidArgumentException(
sprintf('Provider must return ProviderMetadata from metadata() method: %s', $className)
);
}

$this->providerClassNames[$metadata->getId()] = $className;
}

/**
* Checks if a provider is registered.
*
* @since n.e.x.t
*
* @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name to check.
* @return bool True if the provider is registered.
*/
public function hasProvider(string $idOrClassName): bool
{
return isset($this->providerClassNames[$idOrClassName]) ||
in_array($idOrClassName, $this->providerClassNames, true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in_array is not as efficient as using isset. Since this method may be commonly called, I think it would be best to add another class property keyed by the provider class names. This way you can use isset for the second check here too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved here 6e25240

}

/**
* Gets the class name for a registered provider.
*
* @since n.e.x.t
*
* @param string $id The provider ID.
* @return string The provider class name.
* @throws InvalidArgumentException If the provider is not registered.
*/
public function getProviderClassName(string $id): string
{
if (!isset($this->providerClassNames[$id])) {
throw new InvalidArgumentException(
sprintf('Provider not registered: %s', $id)
);
}

return $this->providerClassNames[$id];
}

/**
* Checks if a provider is properly configured.
*
* @since n.e.x.t
*
* @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name.
* @return bool True if the provider is configured and ready to use.
*/
public function isProviderConfigured(string $idOrClassName): bool
{
try {
$className = $this->resolveProviderClassName($idOrClassName);

// Use static method from ProviderInterface
/** @var class-string<ProviderInterface> $className */
$availability = $className::availability();

return $availability->isConfigured();
} catch (InvalidArgumentException $e) {
return false;
}
}

/**
* Finds models across all providers that support the given requirements.
*
* @since n.e.x.t
*
* @param ModelRequirements $modelRequirements The requirements to match against.
* @return list<ProviderModelsMetadata> List of provider models metadata that match requirements.
*/
public function findModelsMetadataForSupport(ModelRequirements $modelRequirements): array
{
$results = [];

foreach ($this->providerClassNames as $providerId => $className) {
$providerResults = $this->findProviderModelsMetadataForSupport($providerId, $modelRequirements);
if (!empty($providerResults)) {
// Use static method from ProviderInterface
/** @var class-string<ProviderInterface> $className */
$providerMetadata = $className::metadata();

$results[] = new ProviderModelsMetadata(
$providerMetadata,
$providerResults
);
}
}

return $results;
}

/**
* Finds models within a specific provider that support the given requirements.
*
* @since n.e.x.t
*
* @param string $idOrClassName The provider ID or class name.
* @param ModelRequirements $modelRequirements The requirements to match against.
* @return list<ModelMetadata> List of model metadata that match requirements.
*/
public function findProviderModelsMetadataForSupport(
string $idOrClassName,
ModelRequirements $modelRequirements
): array {
$className = $this->resolveProviderClassName($idOrClassName);

$modelMetadataDirectory = $className::modelMetadataDirectory();

// Filter models that meet requirements
$matchingModels = [];
foreach ($modelMetadataDirectory->listModelMetadata() as $modelMetadata) {
if ($modelMetadata->meetsRequirements($modelRequirements)) {
$matchingModels[] = $modelMetadata;
}
}

return $matchingModels;
}

/**
* Gets a configured model instance from a provider.
*
* @since n.e.x.t
*
* @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name.
* @param string $modelId The model identifier.
* @param ModelConfig|null $modelConfig The model configuration.
* @return ModelInterface The configured model instance.
* @throws InvalidArgumentException If provider or model is not found.
*/
public function getProviderModel(
string $idOrClassName,
string $modelId,
?ModelConfig $modelConfig = null
): ModelInterface {
$className = $this->resolveProviderClassName($idOrClassName);

// Use static method from ProviderInterface
/** @var class-string<ProviderInterface> $className */
return $className::model($modelId, $modelConfig);
}

/**
* Gets the class name for a registered provider (handles both ID and class name input).
*
* @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name.
* @return class-string<ProviderInterface> The provider class name.
* @throws InvalidArgumentException If provider is not registered.
*/
private function resolveProviderClassName(string $idOrClassName): string
{
// Handle both ID and class name
$className = $this->providerClassNames[$idOrClassName] ?? $idOrClassName;

if (!$this->hasProvider($idOrClassName)) {
throw new InvalidArgumentException(
sprintf('Provider not registered: %s', $idOrClassName)
);
}

// Validate that class implements ProviderInterface (for PHPStan type safety)
if (!is_subclass_of($className, ProviderInterface::class)) {
throw new InvalidArgumentException(
sprintf('Provider class must implement %s: %s', ProviderInterface::class, $className)
);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unnecessary. It's impossible for $className to not implement ProviderInterface, since it's already enforced at the registration level, which is the only way to modify this property.

If PHPStan complains about this, we should rather ignore that because it's an invalid complaint I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@felixarntz resolved here f868e7b


return $className;
}
}
63 changes: 63 additions & 0 deletions tests/mocks/MockModel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

namespace WordPress\AiClient\Tests\mocks;

use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;

/**
* Mock model for testing.
*
* @since n.e.x.t
*/
class MockModel implements ModelInterface
{
/**
* @var ModelMetadata The model metadata.
*/
private ModelMetadata $metadata;

/**
* @var ModelConfig The model configuration.
*/
private ModelConfig $config;

/**
* Constructor.
*
* @param ModelMetadata $metadata The model metadata.
* @param ModelConfig $config The model configuration.
*/
public function __construct(ModelMetadata $metadata, ModelConfig $config)
{
$this->metadata = $metadata;
$this->config = $config;
}

/**
* {@inheritDoc}
*/
public function metadata(): ModelMetadata
{
return $this->metadata;
}

/**
* {@inheritDoc}
*/
public function getConfig(): ModelConfig
{
return $this->config;
}

/**
* {@inheritDoc}
*/
public function setConfig(ModelConfig $config): void
{
$this->config = $config;
}
}
62 changes: 62 additions & 0 deletions tests/mocks/MockModelMetadataDirectory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

namespace WordPress\AiClient\Tests\mocks;

use InvalidArgumentException;
use WordPress\AiClient\Providers\Contracts\ModelMetadataDirectoryInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;

/**
* Mock model metadata directory for testing.
*
* @since n.e.x.t
*/
class MockModelMetadataDirectory implements ModelMetadataDirectoryInterface
{
/**
* @var array<string, ModelMetadata> Available models.
*/
private array $models = [];

/**
* Constructor.
*
* @param array<string, ModelMetadata> $models Available models.
*/
public function __construct(array $models = [])
{
$this->models = $models;
}

/**
* {@inheritDoc}
*/
public function listModelMetadata(): array
{
return array_values($this->models);
}

/**
* {@inheritDoc}
*/
public function hasModelMetadata(string $modelId): bool
{
return isset($this->models[$modelId]);
}

/**
* {@inheritDoc}
*/
public function getModelMetadata(string $modelId): ModelMetadata
{
if (!isset($this->models[$modelId])) {
throw new InvalidArgumentException(
sprintf('Model not found: %s', $modelId)
);
}

return $this->models[$modelId];
}
}
Loading