Completed
Push — master ( 40d00c...eb5fc3 )
by Levan
02:35
created

TranslateClient::setUrlBase()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 6
rs 9.4285
cc 1
eloc 3
nc 1
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 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)) {
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 != '') {
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, dont create loop that will end with
381
        // invalide foreach and warning
382
        if ($isArray || !is_string($responseArray)) {
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 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...
417
        }
418
        // the response can be sometimes an translated string.
419
        elseif (is_string($responseArray)) {
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