Skip to content

Commit ef7d83a

Browse files
Merge pull request #38 from Ref34t/feature/provider-registry
2 parents 7dcd982 + f868e7b commit ef7d83a

File tree

6 files changed

+725
-0
lines changed

6 files changed

+725
-0
lines changed

src/Providers/ProviderRegistry.php

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WordPress\AiClient\Providers;
6+
7+
use InvalidArgumentException;
8+
use WordPress\AiClient\Providers\Contracts\ProviderInterface;
9+
use WordPress\AiClient\Providers\DTO\ProviderMetadata;
10+
use WordPress\AiClient\Providers\DTO\ProviderModelsMetadata;
11+
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
12+
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
13+
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
14+
use WordPress\AiClient\Providers\Models\DTO\ModelRequirements;
15+
16+
/**
17+
* Registry for managing AI providers and their models.
18+
*
19+
* This class provides a centralized way to register AI providers, discover
20+
* their capabilities, and find suitable models based on requirements.
21+
*
22+
* @since n.e.x.t
23+
*/
24+
class ProviderRegistry
25+
{
26+
/**
27+
* @var array<string, class-string<ProviderInterface>> Mapping of provider IDs to class names.
28+
*/
29+
private array $providerClassNames = [];
30+
31+
/**
32+
* @var array<class-string<ProviderInterface>, true> Set of registered class names for fast lookup.
33+
*/
34+
private array $registeredClassNames = [];
35+
36+
37+
/**
38+
* Registers a provider class with the registry.
39+
*
40+
* @since n.e.x.t
41+
*
42+
* @param class-string<ProviderInterface> $className The fully qualified provider class name implementing the
43+
* ProviderInterface
44+
* @throws InvalidArgumentException If the class doesn't exist or implement the required interface.
45+
*/
46+
public function registerProvider(string $className): void
47+
{
48+
if (!class_exists($className)) {
49+
throw new InvalidArgumentException(
50+
sprintf('Provider class does not exist: %s', $className)
51+
);
52+
}
53+
54+
// Validate that class implements ProviderInterface
55+
if (!is_subclass_of($className, ProviderInterface::class)) {
56+
throw new InvalidArgumentException(
57+
sprintf('Provider class must implement %s: %s', ProviderInterface::class, $className)
58+
);
59+
}
60+
61+
$metadata = $className::metadata();
62+
63+
if (!$metadata instanceof ProviderMetadata) {
64+
throw new InvalidArgumentException(
65+
sprintf('Provider must return ProviderMetadata from metadata() method: %s', $className)
66+
);
67+
}
68+
69+
$this->providerClassNames[$metadata->getId()] = $className;
70+
$this->registeredClassNames[$className] = true;
71+
}
72+
73+
/**
74+
* Checks if a provider is registered.
75+
*
76+
* @since n.e.x.t
77+
*
78+
* @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name to check.
79+
* @return bool True if the provider is registered.
80+
*/
81+
public function hasProvider(string $idOrClassName): bool
82+
{
83+
return isset($this->providerClassNames[$idOrClassName]) ||
84+
isset($this->registeredClassNames[$idOrClassName]);
85+
}
86+
87+
/**
88+
* Gets the class name for a registered provider.
89+
*
90+
* @since n.e.x.t
91+
*
92+
* @param string $id The provider ID.
93+
* @return string The provider class name.
94+
* @throws InvalidArgumentException If the provider is not registered.
95+
*/
96+
public function getProviderClassName(string $id): string
97+
{
98+
if (!isset($this->providerClassNames[$id])) {
99+
throw new InvalidArgumentException(
100+
sprintf('Provider not registered: %s', $id)
101+
);
102+
}
103+
104+
return $this->providerClassNames[$id];
105+
}
106+
107+
/**
108+
* Checks if a provider is properly configured.
109+
*
110+
* @since n.e.x.t
111+
*
112+
* @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name.
113+
* @return bool True if the provider is configured and ready to use.
114+
*/
115+
public function isProviderConfigured(string $idOrClassName): bool
116+
{
117+
try {
118+
$className = $this->resolveProviderClassName($idOrClassName);
119+
120+
// Use static method from ProviderInterface
121+
/** @var class-string<ProviderInterface> $className */
122+
$availability = $className::availability();
123+
124+
return $availability->isConfigured();
125+
} catch (InvalidArgumentException $e) {
126+
return false;
127+
}
128+
}
129+
130+
/**
131+
* Finds models across all providers that support the given requirements.
132+
*
133+
* @since n.e.x.t
134+
*
135+
* @param ModelRequirements $modelRequirements The requirements to match against.
136+
* @return list<ProviderModelsMetadata> List of provider models metadata that match requirements.
137+
*/
138+
public function findModelsMetadataForSupport(ModelRequirements $modelRequirements): array
139+
{
140+
$results = [];
141+
142+
foreach ($this->providerClassNames as $providerId => $className) {
143+
$providerResults = $this->findProviderModelsMetadataForSupport($providerId, $modelRequirements);
144+
if (!empty($providerResults)) {
145+
// Use static method from ProviderInterface
146+
/** @var class-string<ProviderInterface> $className */
147+
$providerMetadata = $className::metadata();
148+
149+
$results[] = new ProviderModelsMetadata(
150+
$providerMetadata,
151+
$providerResults
152+
);
153+
}
154+
}
155+
156+
return $results;
157+
}
158+
159+
/**
160+
* Finds models within a specific provider that support the given requirements.
161+
*
162+
* @since n.e.x.t
163+
*
164+
* @param string $idOrClassName The provider ID or class name.
165+
* @param ModelRequirements $modelRequirements The requirements to match against.
166+
* @return list<ModelMetadata> List of model metadata that match requirements.
167+
*/
168+
public function findProviderModelsMetadataForSupport(
169+
string $idOrClassName,
170+
ModelRequirements $modelRequirements
171+
): array {
172+
$className = $this->resolveProviderClassName($idOrClassName);
173+
174+
$modelMetadataDirectory = $className::modelMetadataDirectory();
175+
176+
// Filter models that meet requirements
177+
$matchingModels = [];
178+
foreach ($modelMetadataDirectory->listModelMetadata() as $modelMetadata) {
179+
if ($modelMetadata->meetsRequirements($modelRequirements)) {
180+
$matchingModels[] = $modelMetadata;
181+
}
182+
}
183+
184+
return $matchingModels;
185+
}
186+
187+
/**
188+
* Gets a configured model instance from a provider.
189+
*
190+
* @since n.e.x.t
191+
*
192+
* @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name.
193+
* @param string $modelId The model identifier.
194+
* @param ModelConfig|null $modelConfig The model configuration.
195+
* @return ModelInterface The configured model instance.
196+
* @throws InvalidArgumentException If provider or model is not found.
197+
*/
198+
public function getProviderModel(
199+
string $idOrClassName,
200+
string $modelId,
201+
?ModelConfig $modelConfig = null
202+
): ModelInterface {
203+
$className = $this->resolveProviderClassName($idOrClassName);
204+
205+
// Use static method from ProviderInterface
206+
/** @var class-string<ProviderInterface> $className */
207+
return $className::model($modelId, $modelConfig);
208+
}
209+
210+
/**
211+
* Gets the class name for a registered provider (handles both ID and class name input).
212+
*
213+
* @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name.
214+
* @return class-string<ProviderInterface> The provider class name.
215+
* @throws InvalidArgumentException If provider is not registered.
216+
*/
217+
private function resolveProviderClassName(string $idOrClassName): string
218+
{
219+
// Handle both ID and class name
220+
$className = $this->providerClassNames[$idOrClassName] ?? $idOrClassName;
221+
222+
if (!$this->hasProvider($idOrClassName)) {
223+
throw new InvalidArgumentException(
224+
sprintf('Provider not registered: %s', $idOrClassName)
225+
);
226+
}
227+
228+
// @phpstan-ignore-next-line return.type (Interface implementation guaranteed by registration validation)
229+
return $className;
230+
}
231+
}

tests/mocks/MockModel.php

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WordPress\AiClient\Tests\mocks;
6+
7+
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
8+
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
9+
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
10+
11+
/**
12+
* Mock model for testing.
13+
*
14+
* @since n.e.x.t
15+
*/
16+
class MockModel implements ModelInterface
17+
{
18+
/**
19+
* @var ModelMetadata The model metadata.
20+
*/
21+
private ModelMetadata $metadata;
22+
23+
/**
24+
* @var ModelConfig The model configuration.
25+
*/
26+
private ModelConfig $config;
27+
28+
/**
29+
* Constructor.
30+
*
31+
* @param ModelMetadata $metadata The model metadata.
32+
* @param ModelConfig $config The model configuration.
33+
*/
34+
public function __construct(ModelMetadata $metadata, ModelConfig $config)
35+
{
36+
$this->metadata = $metadata;
37+
$this->config = $config;
38+
}
39+
40+
/**
41+
* {@inheritDoc}
42+
*/
43+
public function metadata(): ModelMetadata
44+
{
45+
return $this->metadata;
46+
}
47+
48+
/**
49+
* {@inheritDoc}
50+
*/
51+
public function getConfig(): ModelConfig
52+
{
53+
return $this->config;
54+
}
55+
56+
/**
57+
* {@inheritDoc}
58+
*/
59+
public function setConfig(ModelConfig $config): void
60+
{
61+
$this->config = $config;
62+
}
63+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WordPress\AiClient\Tests\mocks;
6+
7+
use InvalidArgumentException;
8+
use WordPress\AiClient\Providers\Contracts\ModelMetadataDirectoryInterface;
9+
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
10+
11+
/**
12+
* Mock model metadata directory for testing.
13+
*
14+
* @since n.e.x.t
15+
*/
16+
class MockModelMetadataDirectory implements ModelMetadataDirectoryInterface
17+
{
18+
/**
19+
* @var array<string, ModelMetadata> Available models.
20+
*/
21+
private array $models = [];
22+
23+
/**
24+
* Constructor.
25+
*
26+
* @param array<string, ModelMetadata> $models Available models.
27+
*/
28+
public function __construct(array $models = [])
29+
{
30+
$this->models = $models;
31+
}
32+
33+
/**
34+
* {@inheritDoc}
35+
*/
36+
public function listModelMetadata(): array
37+
{
38+
return array_values($this->models);
39+
}
40+
41+
/**
42+
* {@inheritDoc}
43+
*/
44+
public function hasModelMetadata(string $modelId): bool
45+
{
46+
return isset($this->models[$modelId]);
47+
}
48+
49+
/**
50+
* {@inheritDoc}
51+
*/
52+
public function getModelMetadata(string $modelId): ModelMetadata
53+
{
54+
if (!isset($this->models[$modelId])) {
55+
throw new InvalidArgumentException(
56+
sprintf('Model not found: %s', $modelId)
57+
);
58+
}
59+
60+
return $this->models[$modelId];
61+
}
62+
}

0 commit comments

Comments
 (0)