Intégration de l’API OpenAI dans Symfony
Intégration de l’API OpenAI dans Symfony : Guide complet avec bonnes pratiques
L’intégration de l’intelligence artificielle dans les applications web est devenue incontournable. Ce guide détaille comment intégrer proprement l’API OpenAI dans un projet Symfony en respectant les bonnes pratiques du framework et en utilisant la bibliothèque PHP officielle d’OpenAI.
Prérequis
- Symfony 6.0 ou supérieur
- PHP 8.1 ou supérieur
- Composer
- Une clé API OpenAI valide
Installation des dépendances
Commencez par installer la bibliothèque officielle OpenAI pour PHP :
composer require openai-php/client
Pour une meilleure intégration HTTP, installez également le client HTTP Symfony :
composer require symfony/http-client
Configuration de l’environnement
1. Variables d’environnement
Ajoutez votre clé API OpenAI dans votre fichier .env
:
# .env
OPENAI_API_KEY=sk-your-api-key-here
OPENAI_ORGANIZATION=org-your-organization-id # Optionnel
2. Configuration des services
Créez un fichier de configuration pour OpenAI :
# config/packages/openai.yaml
parameters:
openai.api_key: '%env(OPENAI_API_KEY)%'
openai.organization: '%env(OPENAI_ORGANIZATION)%'
services:
OpenAI\Client:
factory: ['OpenAI', 'client']
arguments:
- '%openai.api_key%'
- '%openai.organization%'
public: false
# Service personnalisé pour encapsuler la logique OpenAI
App\Service\OpenAIService:
arguments:
- '@OpenAI\Client'
tags:
- { name: 'monolog.logger', channel: 'openai' }
Création du service OpenAI
Créez un service dédié pour encapsuler toutes les interactions avec OpenAI :
<?php
// src/Service/OpenAIService.php
namespace App\Service;
use OpenAI\Client;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class OpenAIService
{
public function __construct(
private readonly Client $openAIClient,
#[Autowire(service: 'monolog.logger.openai')]
private readonly LoggerInterface $logger
) {
}
public function generateCompletion(
string $prompt,
string $model = 'gpt-3.5-turbo',
int $maxTokens = 150,
float $temperature = 0.7
): ?string {
try {
$this->logger->info('Génération de completion OpenAI', [
'model' => $model,
'prompt_length' => strlen($prompt),
'max_tokens' => $maxTokens
]);
$response = $this->openAIClient->chat()->create([
'model' => $model,
'messages' => [
['role' => 'user', 'content' => $prompt],
],
'max_tokens' => $maxTokens,
'temperature' => $temperature,
]);
$content = $response->choices[0]->message->content;
$this->logger->info('Completion générée avec succès', [
'response_length' => strlen($content),
'tokens_used' => $response->usage->totalTokens
]);
return $content;
} catch (\Exception $e) {
$this->logger->error('Erreur lors de la génération de completion', [
'error' => $e->getMessage(),
'prompt' => substr($prompt, 0, 100) . '...'
]);
throw new OpenAIException('Erreur lors de la génération de texte', 0, $e);
}
}
public function generateImage(
string $prompt,
string $size = '1024x1024',
int $n = 1
): array {
try {
$this->logger->info('Génération d\'image OpenAI', [
'prompt' => $prompt,
'size' => $size,
'count' => $n
]);
$response = $this->openAIClient->images()->create([
'prompt' => $prompt,
'n' => $n,
'size' => $size,
'response_format' => 'url',
]);
$urls = array_map(fn($image) => $image->url, $response->data);
$this->logger->info('Images générées avec succès', [
'count' => count($urls)
]);
return $urls;
} catch (\Exception $e) {
$this->logger->error('Erreur lors de la génération d\'image', [
'error' => $e->getMessage(),
'prompt' => $prompt
]);
throw new OpenAIException('Erreur lors de la génération d\'image', 0, $e);
}
}
public function moderateContent(string $content): bool
{
try {
$response = $this->openAIClient->moderations()->create([
'input' => $content,
]);
$flagged = $response->results[0]->flagged;
$this->logger->info('Modération de contenu', [
'flagged' => $flagged,
'categories' => $response->results[0]->categories
]);
return !$flagged;
} catch (\Exception $e) {
$this->logger->error('Erreur lors de la modération', [
'error' => $e->getMessage()
]);
// En cas d'erreur, on considère le contenu comme non modéré
return false;
}
}
}
Création d’une exception personnalisée
<?php
// src/Exception/OpenAIException.php
namespace App\Exception;
class OpenAIException extends \Exception
{
}
Utilisation dans un contrôleur
Voici comment utiliser le service dans un contrôleur :
<?php
// src/Controller/AIController.php
namespace App\Controller;
use App\Service\OpenAIService;
use App\Exception\OpenAIException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Validator\ValidatorInterface;
#[Route('/api/ai', name: 'api_ai_')]
class AIController extends AbstractController
{
public function __construct(
private readonly OpenAIService $openAIService,
private readonly ValidatorInterface $validator
) {
}
#[Route('/generate-text', name: 'generate_text', methods: ['POST'])]
public function generateText(Request $request): JsonResponse
{
$data = json_decode($request->getContent(), true);
// Validation des données
$violations = $this->validator->validate($data, new Assert\Collection([
'prompt' => [
new Assert\NotBlank(),
new Assert\Type('string'),
new Assert\Length(['min' => 1, 'max' => 4000])
],
'model' => [
new Assert\Optional(),
new Assert\Choice(['gpt-3.5-turbo', 'gpt-4', 'gpt-4-turbo'])
],
'max_tokens' => [
new Assert\Optional(),
new Assert\Type('integer'),
new Assert\Range(['min' => 1, 'max' => 4000])
]
]));
if (count($violations) > 0) {
return $this->json([
'error' => 'Données invalides',
'violations' => array_map(
fn($violation) => $violation->getMessage(),
iterator_to_array($violations)
)
], Response::HTTP_BAD_REQUEST);
}
try {
// Modération du contenu avant traitement
if (!$this->openAIService->moderateContent($data['prompt'])) {
return $this->json([
'error' => 'Contenu non autorisé'
], Response::HTTP_FORBIDDEN);
}
$result = $this->openAIService->generateCompletion(
$data['prompt'],
$data['model'] ?? 'gpt-3.5-turbo',
$data['max_tokens'] ?? 150
);
return $this->json([
'success' => true,
'result' => $result
]);
} catch (OpenAIException $e) {
return $this->json([
'error' => 'Erreur lors de la génération'
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
#[Route('/generate-image', name: 'generate_image', methods: ['POST'])]
public function generateImage(Request $request): JsonResponse
{
$data = json_decode($request->getContent(), true);
$violations = $this->validator->validate($data, new Assert\Collection([
'prompt' => [
new Assert\NotBlank(),
new Assert\Type('string'),
new Assert\Length(['min' => 1, 'max' => 1000])
],
'size' => [
new Assert\Optional(),
new Assert\Choice(['256x256', '512x512', '1024x1024'])
]
]));
if (count($violations) > 0) {
return $this->json([
'error' => 'Données invalides'
], Response::HTTP_BAD_REQUEST);
}
try {
if (!$this->openAIService->moderateContent($data['prompt'])) {
return $this->json([
'error' => 'Contenu non autorisé'
], Response::HTTP_FORBIDDEN);
}
$imageUrls = $this->openAIService->generateImage(
$data['prompt'],
$data['size'] ?? '1024x1024'
);
return $this->json([
'success' => true,
'images' => $imageUrls
]);
} catch (OpenAIException $e) {
return $this->json([
'error' => 'Erreur lors de la génération d\'image'
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
}
Gestion du cache avec Redis
Pour optimiser les performances et réduire les coûts, implémentez un système de cache :
<?php
// src/Service/CachedOpenAIService.php
namespace App\Service;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
class CachedOpenAIService
{
public function __construct(
private readonly OpenAIService $openAIService,
private readonly CacheInterface $cache
) {
}
public function generateCompletionCached(
string $prompt,
string $model = 'gpt-3.5-turbo',
int $maxTokens = 150,
float $temperature = 0.7,
int $cacheTtl = 3600
): ?string {
$cacheKey = 'openai_completion_' . md5($prompt . $model . $maxTokens . $temperature);
return $this->cache->get($cacheKey, function (ItemInterface $item) use (
$prompt, $model, $maxTokens, $temperature, $cacheTtl
) {
$item->expiresAfter($cacheTtl);
return $this->openAIService->generateCompletion(
$prompt,
$model,
$maxTokens,
$temperature
);
});
}
}
Configuration de la sécurité
Ajoutez une limite de débit pour éviter les abus :
# config/packages/security.yaml
security:
# ... autres configurations
access_control:
- { path: ^/api/ai, roles: ROLE_USER }
# config/packages/framework.yaml
framework:
rate_limiter:
openai_api:
policy: 'sliding_window'
limit: 10
interval: '1 minute'
storage: 'cache.rate_limiter'
Utilisez le rate limiter dans votre contrôleur :
use Symfony\Component\RateLimiter\RateLimiterFactory;
public function __construct(
private readonly OpenAIService $openAIService,
private readonly ValidatorInterface $validator,
private readonly RateLimiterFactory $openaiApiLimiter
) {
}
#[Route('/generate-text', name: 'generate_text', methods: ['POST'])]
public function generateText(Request $request): JsonResponse
{
$limiter = $this->openaiApiLimiter->create($request->getClientIp());
if (!$limiter->consume(1)->isAccepted()) {
return $this->json([
'error' => 'Trop de requêtes'
], Response::HTTP_TOO_MANY_REQUESTS);
}
// ... reste du code
}
Tests unitaires
Créez des tests pour votre service :
<?php
// tests/Service/OpenAIServiceTest.php
namespace App\Tests\Service;
use App\Service\OpenAIService;
use App\Exception\OpenAIException;
use OpenAI\Client;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class OpenAIServiceTest extends TestCase
{
private $openAIClient;
private $logger;
private $openAIService;
protected function setUp(): void
{
$this->openAIClient = $this->createMock(Client::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->openAIService = new OpenAIService($this->openAIClient, $this->logger);
}
public function testGenerateCompletionSuccess(): void
{
$mockResponse = (object) [
'choices' => [
(object) ['message' => (object) ['content' => 'Test response']],
],
'usage' => (object) ['totalTokens' => 10]
];
$this->openAIClient->expects($this->once())
->method('chat')
->willReturn($this->createMockChatClient($mockResponse));
$result = $this->openAIService->generateCompletion('Test prompt');
$this->assertEquals('Test response', $result);
}
public function testGenerateCompletionThrowsException(): void
{
$this->openAIClient->expects($this->once())
->method('chat')
->willThrowException(new \Exception('API Error'));
$this->expectException(OpenAIException::class);
$this->openAIService->generateCompletion('Test prompt');
}
private function createMockChatClient($response)
{
$chatClient = $this->createMock(\OpenAI\Resources\Chat::class);
$chatClient->expects($this->once())
->method('create')
->willReturn($response);
return $chatClient;
}
}
Bonnes pratiques résumées
- Séparation des responsabilités : Utilisez des services dédiés pour encapsuler la logique OpenAI
- Gestion des erreurs : Créez des exceptions personnalisées et loggez les erreurs
- Validation : Validez toujours les données d’entrée
- Sécurité : Implémentez la modération de contenu et les limites de débit
- Performance : Utilisez le cache pour les requêtes répétitives
- Configuration : Externalisez les paramètres dans les variables d’environnement
- Tests : Écrivez des tests unitaires pour vos services
- Monitoring : Ajoutez des logs détaillés pour le debugging et le monitoring
Conclusion
Cette intégration respecte les bonnes pratiques Symfony en utilisant l’injection de dépendances, la validation, la gestion des erreurs et la configuration appropriée. Le service OpenAI est maintenant prêt à être utilisé dans votre application Symfony de manière sécurisée et performante.
N’oubliez pas de surveiller vos coûts d’utilisation de l’API OpenAI et d’adapter les paramètres selon vos besoins spécifiques