TranslateClient::instanceTranslate()   F
last analyzed

Complexity

Conditions 19
Paths 183

Size

Total Lines 76
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 37
dl 0
loc 76
c 0
b 0
f 0
rs 3.825
cc 19
nc 183
nop 1

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
namespace Stichoza\GoogleTranslate;
4
5
use BadMethodCallException;
6
use ErrorException;
7
use Exception;
8
use GuzzleHttp\Client as GuzzleHttpClient;
0 ignored issues
show
Bug introduced by
The type GuzzleHttp\Client was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
9
use GuzzleHttp\Exception\RequestException as GuzzleRequestException;
0 ignored issues
show
Bug introduced by
The type GuzzleHttp\Exception\RequestException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
10
use InvalidArgumentException;
11
use ReflectionClass;
12
use Stichoza\GoogleTranslate\Tokens\GoogleTokenGenerator;
13
use Stichoza\GoogleTranslate\Tokens\TokenProviderInterface;
14
use UnexpectedValueException;
15
16
/**
17
 * Free Google Translate API PHP Package.
18
 *
19
 * @author      Levan Velijanashvili <[email protected]>
20
 *
21
 * @link        http://stichoza.com/
22
 *
23
 * @license     MIT
24
 *
25
 * @method string getLastDetectedSource() Can be called statically too.
26
 * @method string translate(string $text) Can be called statically with signature
27
 *                                        string translate(string $source, string $target, string $text)
28
 */
29
class TranslateClient
30
{
31
    /**
32
     * @var TranslateClient Because nobody cares about singletons
33
     */
34
    private static $staticInstance;
35
36
    /**
37
     * @var \GuzzleHttp\Client HTTP Client
38
     */
39
    private $httpClient;
40
41
    /**
42
     * @var string Source language - from where the string should be translated
43
     */
44
    private $sourceLanguage;
45
46
    /**
47
     * @var string Target language - to which language string should be translated
48
     */
49
    private $targetLanguage;
50
51
    /**
52
     * @var string|bool Last detected source language
53
     */
54
    private static $lastDetectedSource;
55
56
    /**
57
     * @var string Google Translate URL base
58
     */
59
    private $urlBase = 'https://translate.google.com/translate_a/single';
60
61
    /**
62
     * @var array Dynamic guzzleHTTP client options
63
     */
64
    private $httpOptions = [];
65
66
    /**
67
     * @var array URL Parameters
68
     */
69
    private $urlParams = [
70
        'client'   => 't',
71
        'hl'       => 'en',
72
        'dt'       => 't',
73
        'sl'       => null, // Source language
74
        'tl'       => null, // Target language
75
        'q'        => null, // String to translate
76
        'ie'       => 'UTF-8', // Input encoding
77
        'oe'       => 'UTF-8', // Output encoding
78
        'multires' => 1,
79
        'otf'      => 0,
80
        'pc'       => 1,
81
        'trs'      => 1,
82
        'ssel'     => 0,
83
        'tsel'     => 0,
84
        'kc'       => 1,
85
        'tk'       => null,
86
    ];
87
88
    /**
89
     * @var array Regex key-value patterns to replace on response data
90
     */
91
    private $resultRegexes = [
92
        '/,+/'  => ',',
93
        '/\[,/' => '[',
94
    ];
95
96
    /**
97
     * @var TokenProviderInterface
98
     */
99
    private $tokenProvider;
100
101
    /**
102
     * @var string Default token generator class name
103
     */
104
    private $defaultTokenProvider = GoogleTokenGenerator::class;
105
106
    /**
107
     * Class constructor.
108
     *
109
     * For more information about HTTP client configuration options, visit
110
     * "Creating a client" section of GuzzleHttp docs.
111
     * 5.x - http://guzzle.readthedocs.org/en/5.3/clients.html#creating-a-client
112
     *
113
     * @param string $source  Source language (Optional)
114
     * @param string $target  Target language (Optional)
115
     * @param array  $options Associative array of http client configuration options (Optional)
116
     *
117
     * @throws Exception If token provider does not implement TokenProviderInterface
118
     */
119
    public function __construct($source = null, $target = 'en', $options = [], TokenProviderInterface $tokener = null)
120
    {
121
        $this->httpClient = new GuzzleHttpClient($options); // Create HTTP client
122
        $this->setSource($source)->setTarget($target); // Set languages
123
        $this::$lastDetectedSource = false;
124
125
        if (!isset($tokener)) {
126
            $tokener = $this->defaultTokenProvider;
127
        }
128
129
        $tokenProviderReflection = new ReflectionClass($tokener);
130
131
        if ($tokenProviderReflection->implementsInterface(TokenProviderInterface::class)) {
132
            $this->tokenProvider = $tokenProviderReflection->newInstance();
133
        } else {
134
            throw new Exception('Token provider should implement TokenProviderInterface');
135
        }
136
    }
137
138
    /**
139
     * Override translate method for static call.
140
     *
141
     * @throws BadMethodCallException   If calling nonexistent method
142
     * @throws InvalidArgumentException If parameters are passed incorrectly
143
     * @throws InvalidArgumentException If the provided argument is not of type 'string'
144
     * @throws ErrorException           If the HTTP request fails
145
     * @throws UnexpectedValueException If received data cannot be decoded
146
     */
147
    public static function __callStatic($name, $args)
148
    {
149
        switch ($name) {
150
            case 'translate':
151
                if (count($args) < 3) {
152
                    throw new InvalidArgumentException('Expecting 3 parameters');
153
                }
154
                try {
155
                    $result = self::staticTranslate($args[0], $args[1], $args[2]);
156
                } catch (Exception $e) {
157
                    throw $e;
158
                }
159
160
                return $result;
161
            case 'getLastDetectedSource':
162
                return self::staticGetLastDetectedSource();
163
            default:
164
                throw new BadMethodCallException("Method [{$name}] does not exist");
165
        }
166
    }
167
168
    /**
169
     * Override translate method for instance call.
170
     *
171
     * @throws BadMethodCallException   If calling nonexistent method
172
     * @throws InvalidArgumentException If parameters are passed incorrectly
173
     * @throws InvalidArgumentException If the provided argument is not of type 'string'
174
     * @throws ErrorException           If the HTTP request fails
175
     * @throws UnexpectedValueException If received data cannot be decoded
176
     */
177
    public function __call($name, $args)
178
    {
179
        switch ($name) {
180
            case 'translate':
181
                if (count($args) < 1) {
182
                    throw new InvalidArgumentException('Expecting 1 parameter');
183
                }
184
                try {
185
                    $result = $this->instanceTranslate($args[0]);
186
                } catch (Exception $e) {
187
                    throw $e;
188
                }
189
190
                return $result;
191
            case 'getLastDetectedSource':
192
                return $this::staticGetLastDetectedSource();
193
            case 'getResponse':
194
                // getResponse is available for instanse calls only.
195
                return $this->getResponse($args[0]);
196
            default:
197
                throw new BadMethodCallException("Method [{$name}] does not exist");
198
        }
199
    }
200
201
    /**
202
     * Check if static instance exists and instantiate if not.
203
     *
204
     * @return void
205
     */
206
    private static function checkStaticInstance()
207
    {
208
        if (!isset(self::$staticInstance)) {
209
            self::$staticInstance = new self();
210
        }
211
    }
212
    
213
    /**
214
     * Set the api we are used to translete.
215
     *
216
     * @param string $source Google translate api, default is https://translate.google.com/translate_a/single
217
     *
218
     * @return TranslateClient
219
     */
220
    public function setApi($api = null)
221
    {
222
        if ($api) {
223
            $this->urlBase = $api;
224
        }
225
226
        return $this;
227
    }
228
229
    /**
230
     * Set source language we are translating from.
231
     *
232
     * @param string $source Language code
233
     *
234
     * @return TranslateClient
235
     */
236
    public function setSource($source = null)
237
    {
238
        $this->sourceLanguage = is_null($source) ? 'auto' : $source;
239
240
        return $this;
241
    }
242
243
    /**
244
     * Set translation language we are translating to.
245
     *
246
     * @param string $target Language code
247
     *
248
     * @return TranslateClient
249
     */
250
    public function setTarget($target)
251
    {
252
        $this->targetLanguage = $target;
253
254
        return $this;
255
    }
256
257
    /**
258
     * Set Google Translate URL base
259
     *
260
     * @param string $urlBase  Google Translate URL base
261
     *
262
     * @return TranslateClient
263
     */
264
    public function setUrlBase($urlBase)
265
    {
266
        $this->urlBase = $urlBase;
267
268
        return $this;
269
    }
270
271
    /**
272
     * Set guzzleHttp client options.
273
     *
274
     * @param array $options guzzleHttp client options.
275
     *
276
     * @return TranslateClient
277
     */
278
    public function setHttpOption(array $options)
279
    {
280
        $this->httpOptions = $options;
281
282
        return $this;
283
    }
284
285
    /**
286
     * Get response array.
287
     *
288
     * @param string|array $data String or array of strings to translate
289
     *
290
     * @throws InvalidArgumentException If the provided argument is not of type 'string'
291
     * @throws ErrorException           If the HTTP request fails
292
     * @throws UnexpectedValueException If received data cannot be decoded
293
     *
294
     * @return array Response
295
     */
296
    private function getResponse($data)
297
    {
298
        if (!is_string($data) && !is_array($data)) {
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
299
            throw new InvalidArgumentException('Invalid argument provided');
300
        }
301
302
        $tokenData = is_array($data) ? implode('', $data) : $data;
303
304
        $queryArray = array_merge($this->urlParams, [
305
            'sl'   => $this->sourceLanguage,
306
            'tl'   => $this->targetLanguage,
307
            'tk'   => $this->tokenProvider->generateToken($this->sourceLanguage, $this->targetLanguage, $tokenData),
308
        ]);
309
310
        $queryUrl = preg_replace('/%5B(?:[0-9]|[1-9][0-9]+)%5D=/', '=', http_build_query($queryArray));
311
312
        $queryBodyArray = [
313
            'q' => $data,
314
        ];
315
316
        $queryBodyEncoded = preg_replace('/%5B(?:[0-9]|[1-9][0-9]+)%5D=/', '=', http_build_query($queryBodyArray));
317
318
        try {
319
            $response = $this->httpClient->post($this->urlBase, [
320
                    'query' => $queryUrl,
321
                    'body'  => $queryBodyEncoded,
322
                ] + $this->httpOptions);
323
        } catch (GuzzleRequestException $e) {
324
            throw new ErrorException($e->getMessage());
325
        }
326
327
        $body = $response->getBody(); // Get response body
328
329
        // Modify body to avoid json errors
330
        $bodyJson = preg_replace(array_keys($this->resultRegexes), array_values($this->resultRegexes), $body);
331
332
        // Decode JSON data
333
        if (($bodyArray = json_decode($bodyJson, true)) === null) {
334
            throw new UnexpectedValueException('Data cannot be decoded or it\'s deeper than the recursion limit');
335
        }
336
337
        return $bodyArray;
338
    }
339
340
    /**
341
     * Translate text.
342
     *
343
     * This can be called from instance method translate() using __call() magic method.
344
     * Use $instance->translate($string) instead.
345
     *
346
     * @param string|array $data Text or array of texts to translate
347
     *
348
     * @throws InvalidArgumentException If the provided argument is not of type 'string'
349
     * @throws ErrorException           If the HTTP request fails
350
     * @throws UnexpectedValueException If received data cannot be decoded
351
     *
352
     * @return string|bool Translated text
353
     */
354
    private function instanceTranslate($data)
355
    {
356
        // Whether or not is the data an array
357
        $isArray = is_array($data);
358
359
        // Rethrow exceptions
360
        try {
361
            $responseArray = $this->getResponse($data);
362
        } catch (Exception $e) {
363
            throw $e;
364
        }
365
366
        // if response in text and the content has zero the empty returns true, lets check
367
        // if response is string and not empty and create array for further logic
368
        if (is_string($responseArray) && $responseArray != '') {
0 ignored issues
show
introduced by
The condition is_string($responseArray) is always false.
Loading history...
369
            $responseArray = [$responseArray];
370
        }
371
372
        // Check if translation exists
373
        if (!isset($responseArray[0]) || empty($responseArray[0])) {
374
            return false;
375
        }
376
377
        // Detect languages
378
        $detectedLanguages = [];
379
380
        // the response contains only single translation, don't create loop that will end with
381
        // invalid foreach and warning
382
        if ($isArray || !is_string($responseArray)) {
0 ignored issues
show
introduced by
The condition is_string($responseArray) is always false.
Loading history...
383
            $responseArrayForLanguages = ($isArray) ? $responseArray[0] : [$responseArray];
384
            foreach ($responseArrayForLanguages as $itemArray) {
385
                foreach ($itemArray as $item) {
386
                    if (is_string($item)) {
387
                        $detectedLanguages[] = $item;
388
                    }
389
                }
390
            }
391
        }
392
393
        // Another case of detected language
394
        if (isset($responseArray[count($responseArray) - 2][0][0])) {
395
            $detectedLanguages[] = $responseArray[count($responseArray) - 2][0][0];
396
        }
397
398
        // Set initial detected language to null
399
        $this::$lastDetectedSource = false;
400
401
        // Iterate and set last detected language
402
        foreach ($detectedLanguages as $lang) {
403
            if ($this->isValidLocale($lang)) {
404
                $this::$lastDetectedSource = $lang;
405
                break;
406
            }
407
        }
408
409
        // Reduce array to generate translated sentenece
410
        if ($isArray) {
411
            $carry = [];
412
            foreach ($responseArray[0] as $item) {
413
                $carry[] = $item[0][0][0];
414
            }
415
416
            return $carry;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $carry returns the type array which is incompatible with the documented return type string|boolean.
Loading history...
417
        }
418
        // the response can be sometimes an translated string.
419
        elseif (is_string($responseArray)) {
0 ignored issues
show
introduced by
The condition is_string($responseArray) is always false.
Loading history...
420
            return $responseArray;
421
        } else {
422
            if (is_array($responseArray[0])) {
423
                return array_reduce($responseArray[0], function ($carry, $item) {
424
                    $carry .= $item[0];
425
426
                    return $carry;
427
                });
428
            } else {
429
                return $responseArray[0];
430
            }
431
        }
432
    }
433
434
    /**
435
     * Translate text statically.
436
     *
437
     * This can be called from static method translate() using __callStatic() magic method.
438
     * Use TranslateClient::translate($source, $target, $string) instead.
439
     *
440
     * @param string $source Source language
441
     * @param string $target Target language
442
     * @param string $string Text to translate
443
     *
444
     * @throws InvalidArgumentException If the provided argument is not of type 'string'
445
     * @throws ErrorException           If the HTTP request fails
446
     * @throws UnexpectedValueException If received data cannot be decoded
447
     *
448
     * @return string|bool Translated text
449
     */
450
    private static function staticTranslate($source, $target, $string)
451
    {
452
        self::checkStaticInstance();
453
        try {
454
            $result = self::$staticInstance
455
                ->setSource($source)
456
                ->setTarget($target)
457
                ->translate($string);
458
        } catch (Exception $e) {
459
            throw $e;
460
        }
461
462
        return $result;
463
    }
464
465
    /**
466
     * Get last detected language.
467
     *
468
     * @return string|bool Last detected language or boolean FALSE
469
     */
470
    private static function staticGetLastDetectedSource()
471
    {
472
        return self::$lastDetectedSource;
473
    }
474
475
    /**
476
     * Check if given locale is valid.
477
     *
478
     * @param string $lang Langauge code to verify
479
     *
480
     * @return bool
481
     */
482
    private function isValidLocale($lang)
483
    {
484
        return (bool) preg_match('/^([a-z]{2})(-[A-Z]{2})?$/', $lang);
485
    }
486
}
487