Passed
Push — master ( 0befdb...4ef104 )
by
unknown
12:28 queued 01:07
created

AiProviderFactory::__construct()   D

Complexity

Conditions 24
Paths 108

Size

Total Lines 128
Code Lines 69

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 24
eloc 69
nc 108
nop 4
dl 0
loc 128
rs 4.1
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\AiProvider;
8
9
use Chamilo\CoreBundle\Repository\AiRequestsRepository;
10
use Chamilo\CoreBundle\Settings\SettingsManager;
11
use Exception;
12
use InvalidArgumentException;
13
use Symfony\Bundle\SecurityBundle\Security;
14
use Symfony\Contracts\HttpClient\HttpClientInterface;
15
16
final class AiProviderFactory
17
{
18
    /**
19
     * @var array<string, array<string, object>>
20
     */
21
    private array $providers = [];
22
23
    /**
24
     * @var array<string, array<string, object>>
25
     */
26
    private array $providersByType = [];
27
28
    private string $defaultProvider;
29
30
    public function __construct(
31
        private readonly HttpClientInterface $httpClient,
32
        private readonly SettingsManager $settingsManager,
33
        private readonly AiRequestsRepository $aiRequestsRepository,
34
        private readonly Security $security
35
    ) {
36
        $config = $this->readProvidersConfig();
37
38
        // Default provider = first key in config (if any)
39
        $this->defaultProvider = array_key_first($config) ?? 'openai';
40
41
        // Provider name -> class prefix
42
        $possibleProviders = [
43
            'openai' => 'OpenAi',
44
            'deepseek' => 'DeepSeek',
45
            'grok' => 'Grok',
46
            'mistral' => 'Mistral',
47
            'gemini' => 'Gemini',
48
        ];
49
50
        // Suffix used only if you create type-specific classes (optional)
51
        $typeSuffix = [
52
            'text' => '',
53
            'image' => 'Image',
54
            'video' => 'Video',
55
            'document' => 'Document',
56
            'document_process' => 'DocumentProcess',
57
        ];
58
59
        // Expected interface per known type
60
        $typeInterface = [
61
            'text' => AiProviderInterface::class,
62
            'image' => AiImageProviderInterface::class,
63
            'video' => AiVideoProviderInterface::class,
64
            'document' => AiDocumentProviderInterface::class,
65
            // 'document_process' intentionally not validated (no interface yet)
66
        ];
67
68
        $this->providers = [];
69
        $this->providersByType = [];
70
71
        foreach ($config as $providerName => $providerConfig) {
72
            if (!isset($possibleProviders[$providerName])) {
73
                error_log('[AI] Unsupported provider in config: "'.$providerName.'". Skipping.');
74
                continue;
75
            }
76
77
            if (!\is_array($providerConfig)) {
78
                error_log('[AI] Provider config for "'.$providerName.'" must be an array. Skipping.');
79
                continue;
80
            }
81
82
            $providerPrefix = $possibleProviders[$providerName];
83
84
            // Base provider class (e.g. OpenAiProvider)
85
            $baseClass = 'Chamilo\\CoreBundle\\AiProvider\\'.$providerPrefix.'Provider';
86
            $baseObject = $this->instantiateProvider($baseClass);
87
88
            // If base class cannot be instantiated, we can still allow type-specific classes.
89
            if (!$baseObject && !class_exists($baseClass)) {
90
                error_log('[AI] Base provider class not found: '.$baseClass.'.');
91
            } elseif (!$baseObject) {
92
                error_log('[AI] Base provider class exists but could not be instantiated: '.$baseClass.'.');
93
            }
94
95
            foreach ($typeSuffix as $type => $suffix) {
96
                // Only types explicitly present in provider config are considered enabled
97
                if (!array_key_exists($type, $providerConfig)) {
98
                    continue;
99
                }
100
101
                $typeClass = 'Chamilo\\CoreBundle\\AiProvider\\'.$providerPrefix.$suffix.'Provider';
102
103
                // Known types: we can fallback to base provider if it implements the right interface
104
                if (isset($typeInterface[$type])) {
105
                    $iface = $typeInterface[$type];
106
                    $obj = null;
107
108
                    // 1) Prefer type-specific class if exists (optional architecture)
109
                    if ($typeClass !== $baseClass && class_exists($typeClass)) {
110
                        $obj = $this->instantiateProvider($typeClass);
111
112
                        if ($obj && !($obj instanceof $iface)) {
113
                            error_log('[AI] Provider "'.$providerName.'" type-class "'.$typeClass.'" does not implement '.$iface.'. Falling back to base provider.');
114
                            $obj = null;
115
                        }
116
                    }
117
118
                    // 2) Fallback to base provider
119
                    if (!$obj) {
120
                        if ($baseObject && ($baseObject instanceof $iface)) {
121
                            $obj = $baseObject;
122
123
                            if ($typeClass !== $baseClass && !class_exists($typeClass)) {
124
                                error_log('[AI] Provider "'.$providerName.'" uses base provider for type "'.$type.'" (no dedicated type class found).');
125
                            }
126
                        } else {
127
                            error_log('[AI] Provider "'.$providerName.'" is configured for type "'.$type.'" but no usable implementation was found (expected '.$iface.').');
128
                            continue;
129
                        }
130
                    }
131
132
                    $this->providers[$providerName][$type] = $obj;
133
                    $this->providersByType[$type][$providerName] = $obj;
134
                    continue;
135
                }
136
137
                // Unknown type (e.g. document_process): do NOT fallback to base provider.
138
                // Require a dedicated class to avoid claiming support when methods don't exist.
139
                if ($typeClass !== $baseClass && class_exists($typeClass)) {
140
                    $obj = $this->instantiateProvider($typeClass);
141
142
                    if (!$obj) {
143
                        error_log('[AI] Provider "'.$providerName.'" is configured for unknown type "'.$type.'" but could not instantiate "'.$typeClass.'".');
144
                        continue;
145
                    }
146
147
                    $this->providers[$providerName][$type] = $obj;
148
                    $this->providersByType[$type][$providerName] = $obj;
149
                } else {
150
                    error_log('[AI] Provider "'.$providerName.'" is configured for unknown type "'.$type.'" but no dedicated class was found (expected "'.$typeClass.'").');
151
                }
152
            }
153
        }
154
155
        // Ensure default provider exists when config is not empty
156
        if (!empty($config) && !isset($this->providers[$this->defaultProvider])) {
157
            throw new InvalidArgumentException("The default AI provider '{$this->defaultProvider}' is not configured properly.");
158
        }
159
    }
160
161
    public function hasProvidersForType(string $serviceType): bool
162
    {
163
        return !empty($this->providersByType[$serviceType] ?? []);
164
    }
165
166
    /**
167
     * @return string[] Provider names supporting the given service type
168
     */
169
    public function getProvidersForType(string $serviceType): array
170
    {
171
        return array_keys($this->providersByType[$serviceType] ?? []);
172
    }
173
174
    public function getProvider(?string $provider = null, ?string $serviceType = 'text'): object
175
    {
176
        $serviceType = $serviceType ?? 'text';
177
178
        if (null === $provider) {
179
            if (isset($this->providers[$this->defaultProvider][$serviceType])) {
180
                $provider = $this->defaultProvider;
181
            } else {
182
                $provider = array_key_first($this->providersByType[$serviceType] ?? []);
183
            }
184
        }
185
186
        if (empty($provider) || !isset($this->providers[$provider])) {
187
            throw new InvalidArgumentException("AI Provider '{$provider}' is not supported or not configured.");
188
        }
189
190
        if (!isset($this->providers[$provider][$serviceType])) {
191
            throw new InvalidArgumentException("AI Provider '{$provider}' is not supported for service type '{$serviceType}'.");
192
        }
193
194
        return $this->providers[$provider][$serviceType];
195
    }
196
197
    /**
198
     * Read and normalize JSON config from settings.
199
     *
200
     * @return array<string, mixed>
201
     */
202
    private function readProvidersConfig(): array
203
    {
204
        $configJson = $this->settingsManager->getSetting('ai_helpers.ai_providers', true);
205
206
        if (\is_string($configJson)) {
207
            return json_decode($configJson, true) ?? [];
208
        }
209
210
        if (\is_array($configJson)) {
211
            return $configJson;
212
        }
213
214
        return [];
215
    }
216
217
    /**
218
     * Instantiate a provider class using the unified constructor signature.
219
     */
220
    private function instantiateProvider(string $fqcn): ?object
221
    {
222
        if (!class_exists($fqcn)) {
223
            return null;
224
        }
225
226
        try {
227
            return new $fqcn(
228
                $this->httpClient,
229
                $this->settingsManager,
230
                $this->aiRequestsRepository,
231
                $this->security
232
            );
233
        } catch (Exception $e) {
234
            error_log('[AI] Could not instantiate '.$fqcn.': '.$e->getMessage());
235
            return null;
236
        }
237
    }
238
}
239