Completed
Push — master ( a029d8...40d00c )
by Levan
10s
created

TranslateClient::setApi()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 8
rs 9.4285
cc 2
eloc 4
nc 2
nop 1
1
<?php
2
3
namespace Stichoza\GoogleTranslate;
4
5
use BadMethodCallException;
6
use ErrorException;
7
use Exception;
8
use GuzzleHttp\Client as GuzzleHttpClient;
9
use GuzzleHttp\Exception\RequestException as GuzzleRequestException;
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
0 ignored issues
show
Bug introduced by
There is no parameter named $source. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
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 transleting 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 transleting 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 guzzleHttp client options.
259
     *
260
     * @param array $options guzzleHttp client options.
261
     *
262
     * @return TranslateClient
263
     */
264
    public function setHttpOption(array $options)
265
    {
266
        $this->httpOptions = $options;
267
268
        return $this;
269
    }
270
271
    /**
272
     * Get response array.
273
     *
274
     * @param string|array $data String or array of strings to translate
275
     *
276
     * @throws InvalidArgumentException If the provided argument is not of type 'string'
277
     * @throws ErrorException           If the HTTP request fails
278
     * @throws UnexpectedValueException If received data cannot be decoded
279
     *
280
     * @return array Response
281
     */
282
    private function getResponse($data)
283
    {
284
        if (!is_string($data) && !is_array($data)) {
285
            throw new InvalidArgumentException('Invalid argument provided');
286
        }
287
288
        $tokenData = is_array($data) ? implode('', $data) : $data;
289
290
        $queryArray = array_merge($this->urlParams, [
291
            'sl'   => $this->sourceLanguage,
292
            'tl'   => $this->targetLanguage,
293
            'tk'   => $this->tokenProvider->generateToken($this->sourceLanguage, $this->targetLanguage, $tokenData),
294
        ]);
295
296
        $queryUrl = preg_replace('/%5B(?:[0-9]|[1-9][0-9]+)%5D=/', '=', http_build_query($queryArray));
297
298
        $queryBodyArray = [
299
            'q' => $data,
300
        ];
301
302
        $queryBodyEncoded = preg_replace('/%5B(?:[0-9]|[1-9][0-9]+)%5D=/', '=', http_build_query($queryBodyArray));
303
304
        try {
305
            $response = $this->httpClient->post($this->urlBase, [
306
                    'query' => $queryUrl,
307
                    'body'  => $queryBodyEncoded,
308
                ] + $this->httpOptions);
309
        } catch (GuzzleRequestException $e) {
310
            throw new ErrorException($e->getMessage());
311
        }
312
313
        $body = $response->getBody(); // Get response body
314
315
        // Modify body to avoid json errors
316
        $bodyJson = preg_replace(array_keys($this->resultRegexes), array_values($this->resultRegexes), $body);
317
318
        // Decode JSON data
319
        if (($bodyArray = json_decode($bodyJson, true)) === null) {
320
            throw new UnexpectedValueException('Data cannot be decoded or it\'s deeper than the recursion limit');
321
        }
322
323
        return $bodyArray;
324
    }
325
326
    /**
327
     * Translate text.
328
     *
329
     * This can be called from instance method translate() using __call() magic method.
330
     * Use $instance->translate($string) instead.
331
     *
332
     * @param string|array $data Text or array of texts to translate
333
     *
334
     * @throws InvalidArgumentException If the provided argument is not of type 'string'
335
     * @throws ErrorException           If the HTTP request fails
336
     * @throws UnexpectedValueException If received data cannot be decoded
337
     *
338
     * @return string|bool Translated text
339
     */
340
    private function instanceTranslate($data)
341
    {
342
        // Whether or not is the data an array
343
        $isArray = is_array($data);
344
345
        // Rethrow exceptions
346
        try {
347
            $responseArray = $this->getResponse($data);
348
        } catch (Exception $e) {
349
            throw $e;
350
        }
351
352
        // if response in text and the content has zero the empty returns true, lets check
353
        // if response is string and not empty and create array for further logic
354
        if (is_string($responseArray) && $responseArray != '') {
355
            $responseArray = [$responseArray];
356
        }
357
358
        // Check if translation exists
359
        if (!isset($responseArray[0]) || empty($responseArray[0])) {
360
            return false;
361
        }
362
363
        // Detect languages
364
        $detectedLanguages = [];
365
366
        // the response contains only single translation, dont create loop that will end with
367
        // invalide foreach and warning
368
        if ($isArray || !is_string($responseArray)) {
369
            $responseArrayForLanguages = ($isArray) ? $responseArray[0] : [$responseArray];
370
            foreach ($responseArrayForLanguages as $itemArray) {
371
                foreach ($itemArray as $item) {
372
                    if (is_string($item)) {
373
                        $detectedLanguages[] = $item;
374
                    }
375
                }
376
            }
377
        }
378
379
        // Another case of detected language
380
        if (isset($responseArray[count($responseArray) - 2][0][0])) {
381
            $detectedLanguages[] = $responseArray[count($responseArray) - 2][0][0];
382
        }
383
384
        // Set initial detected language to null
385
        $this::$lastDetectedSource = false;
386
387
        // Iterate and set last detected language
388
        foreach ($detectedLanguages as $lang) {
389
            if ($this->isValidLocale($lang)) {
390
                $this::$lastDetectedSource = $lang;
391
                break;
392
            }
393
        }
394
395
        // Reduce array to generate translated sentenece
396
        if ($isArray) {
397
            $carry = [];
398
            foreach ($responseArray[0] as $item) {
399
                $carry[] = $item[0][0][0];
400
            }
401
402
            return $carry;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $carry; (array) is incompatible with the return type documented by Stichoza\GoogleTranslate...ient::instanceTranslate of type string|boolean.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
403
        }
404
        // the response can be sometimes an translated string.
405
        elseif (is_string($responseArray)) {
406
            return $responseArray;
407
        } else {
408
            if (is_array($responseArray[0])) {
409
                return array_reduce($responseArray[0], function ($carry, $item) {
410
                    $carry .= $item[0];
411
412
                    return $carry;
413
                });
414
            } else {
415
                return $responseArray[0];
416
            }
417
        }
418
    }
419
420
    /**
421
     * Translate text statically.
422
     *
423
     * This can be called from static method translate() using __callStatic() magic method.
424
     * Use TranslateClient::translate($source, $target, $string) instead.
425
     *
426
     * @param string $source Source language
427
     * @param string $target Target language
428
     * @param string $string Text to translate
429
     *
430
     * @throws InvalidArgumentException If the provided argument is not of type 'string'
431
     * @throws ErrorException           If the HTTP request fails
432
     * @throws UnexpectedValueException If received data cannot be decoded
433
     *
434
     * @return string|bool Translated text
435
     */
436
    private static function staticTranslate($source, $target, $string)
437
    {
438
        self::checkStaticInstance();
439
        try {
440
            $result = self::$staticInstance
441
                ->setSource($source)
442
                ->setTarget($target)
443
                ->translate($string);
444
        } catch (Exception $e) {
445
            throw $e;
446
        }
447
448
        return $result;
449
    }
450
451
    /**
452
     * Get last detected language.
453
     *
454
     * @return string|bool Last detected language or boolean FALSE
455
     */
456
    private static function staticGetLastDetectedSource()
457
    {
458
        return self::$lastDetectedSource;
459
    }
460
461
    /**
462
     * Check if given locale is valid.
463
     *
464
     * @param string $lang Langauge code to verify
465
     *
466
     * @return bool
467
     */
468
    private function isValidLocale($lang)
469
    {
470
        return (bool) preg_match('/^([a-z]{2})(-[A-Z]{2})?$/', $lang);
471
    }
472
}
473