Intégration de l’API OpenAI dans Symfony

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

  1. Séparation des responsabilités : Utilisez des services dédiés pour encapsuler la logique OpenAI
  2. Gestion des erreurs : Créez des exceptions personnalisées et loggez les erreurs
  3. Validation : Validez toujours les données d’entrée
  4. Sécurité : Implémentez la modération de contenu et les limites de débit
  5. Performance : Utilisez le cache pour les requêtes répétitives
  6. Configuration : Externalisez les paramètres dans les variables d’environnement
  7. Tests : Écrivez des tests unitaires pour vos services
  8. 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

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *