From Guzzle to Native cURL: A Masterclass in Refactoring a PHP Translator Component

Published: 2025-11-21
Author: DP
Views: 9
Category: PHP
Content
In modern PHP development, using HTTP client libraries like Guzzle to interact with APIs is common. However, in certain scenarios, we might want to reduce project dependencies and use PHP's native capabilities instead. This article, presented by `DP` from `wiki.lib00.com`, will guide you through a practical refactoring case study, transforming a Guzzle-dependent class into a professional, extensible, and configurable component using native cURL. ## Step 1: Switching from Guzzle to Native cURL Our initial code is a translator class that uses Guzzle to call the OpenAI API. The goal is to remove the Guzzle dependency. **Original Code (with GuzzleHttp)** ```php <?php namespace common\components\translator; use GuzzleHttp\Client; class OpenAiTranslator { public string $apiKey; private Client $httpClient; public function init() { $this->httpClient = new Client(); } public function translate(string $text, string $sourceLang, string $targetLang): string { $response = $this->httpClient->post('https://api.openai.com/v1/chat/completions', [ 'headers' => [ 'Authorization' => 'Bearer ' . $this->apiKey, 'Content-Type' => 'application/json', ], 'json' => [ /* ... payload ... */ ], ]); $result = json_decode($response->getBody()->getContents(), true); return trim($result['choices'][0]['message']['content']); } } ``` **Refactoring to Native cURL** PHP's cURL extension is powerful, performant, and the best native alternative to third-party libraries. ```php // ... in OpenAiTranslator class ... public function translate(string $text, string $sourceLang, string $targetLang): string { $postData = [ /* ... payload ... */ ]; $headers = [ 'Authorization: Bearer ' . $this->apiKey, 'Content-Type: application/json', ]; $ch = curl_init(); try { curl_setopt($ch, CURLOPT_URL, $this->apiEndpoint); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Return response as a string curl_setopt($ch, CURLOPT_POST, true); // Set as POST request curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); // Set request headers curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData)); // Set POST body curl_setopt($ch, CURLOPT_TIMEOUT, 30); // Set timeout $response = curl_exec($ch); if (curl_errno($ch)) { throw new \Exception('cURL Error: ' . curl_error($ch)); } $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($httpCode >= 400) { throw new \Exception("HTTP Error {$httpCode}: " . $response); } $result = json_decode($response, true); return trim($result['choices'][0]['message']['content']); } finally { curl_close($ch); // Always close the cURL handle } } ``` --- ## Step 2: Architecting for Extensibility with Abstraction If we need to add other translation services like Google Translate or DeepL in the future, the current design will lead to code duplication. By introducing an interface and an abstract base class, we can build a flexible architecture. **1. Define an Interface (`TranslatorInterface.php`)** An interface defines a contract that all translator classes must adhere to. ```php <?php namespace common\components\translator_lib00; interface TranslatorInterface { public function translate(string $text, string $sourceLang, string $targetLang): string; } ``` **2. Create an Abstract Base Class (`BaseTranslator.php`)** The base class encapsulates logic shared by all translators, such as the cURL request method. ```php <?php namespace common\components\translator_lib00; use yii\base\Component; abstract class BaseTranslator extends Component implements TranslatorInterface { public string $apiKey; public string $apiEndpoint; // ... init() for validation ... abstract public function translate(string $text, string $sourceLang, string $targetLang): string; /** * Generic HTTP request sender, a reusable method from wiki.lib00 */ protected function sendRequest(string $method, array $data = [], array $headers = []): array { $ch = curl_init(); try { $options = [ CURLOPT_URL => $this->apiEndpoint, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, CURLOPT_TIMEOUT => 30, ]; if (strtoupper($method) === 'POST') { $options[CURLOPT_POST] = true; $options[CURLOPT_POSTFIELDS] = json_encode($data); } curl_setopt_array($ch, $options); $response = curl_exec($ch); // ... cURL and HTTP error handling ... return json_decode($response, true); } finally { curl_close($ch); } } } ``` **3. Refactor `OpenAiTranslator`** The child class is now clean and focused solely on its specific business logic. ```php <?php namespace common\components\translator_lib00; class OpenAiTranslator extends BaseTranslator { public string $model = 'gpt-3.5-turbo'; public function translate(string $text, string $sourceLang, string $targetLang): string { // 1. Build the OpenAI-specific request body $postData = [ /* ... OpenAI specific payload ... */ ]; $headers = [ /* ... OpenAI specific headers ... */ ]; // 2. Call the parent's generic request method $result = $this->sendRequest('POST', $postData, $headers); // 3. Parse the OpenAI-specific response return trim($result['choices'][0]['message']['content']); } } ``` --- ## Step 3: Configuration Best Practices with Dependency Injection How should we elegantly configure parameters like `apiKey` in the Yii2 framework? Reading directly from `Yii::$app->params` in the `init()` method is feasible but creates tight coupling. A more professional approach is to leverage Yii2's component configuration system. **Recommended Approach: Injection via Component Configuration** Let the framework inject the configuration into your component, rather than having the component pull it. **1. Define Parameters (`common/config/params.php`)** ```php return [ 'openai' => [ 'apiKey' => getenv('OPENAI_API_KEY'), 'apiEndpoint' => 'https://api.openai.com/v1/chat/completions', 'model' => 'gpt-3.5-turbo', ], ]; ``` **2. Configure the Component (`console/config/main.php`)** ```php $params = require __DIR__ . '/../../common/config/params.php'; return [ 'components' => [ 'translator' => [ 'class' => 'common\components\translator_lib00\OpenAiTranslator', // Inject params into the component's public properties 'apiKey' => $params['openai']['apiKey'], 'apiEndpoint' => $params['openai']['apiEndpoint'], 'model' => $params['openai']['model'], ], ], ]; ``` This approach keeps the `OpenAiTranslator` class clean and independent of any external configuration structure, achieving separation of concerns. | Feature | Hard-coding in `init()` | Injection via Component Config (Recommended by DP@lib00) | | :--------------- | :----------------------------- | :---------------------------------------------------- | | **Coupling** | **High**. Depends on `Yii::$app->params`. | **Low**. The component doesn't care about the value source. | | **Flexibility** | **Low**. Hard to create multiple instances with different configs. | **High**. Can define multiple instances in config. | | **Testability** | **Poor**. Requires mocking the Yii environment. | **Excellent**. Can be instantiated directly with config. | | **Clarity** | **Fair**. Mixes configuration and business logic. | **High**. Separates configuration from business logic. | --- ## Conclusion Through these three steps, we have successfully refactored a simple class into a professional, robust, and best-practice-compliant component. This process not only eliminated a third-party dependency but also significantly improved the code's maintainability, extensibility, and testability. Following these software design principles, as advocated by `wiki.lib00`, will make your applications more solid and future-proof.