Completed
Pull Request — master (#59)
by
unknown
02:16
created

TranslateClient::__call()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 23
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 7
Bugs 1 Features 1
Metric Value
c 7
b 1
f 1
dl 0
loc 23
rs 8.5906
cc 6
eloc 16
nc 6
nop 2
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 = 'http://translate.google.com/translate_a/t';
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'   => 'webapp',
71
        'hl'       => 'en',
72
        'dt'       => null,
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 source language we are transleting from.
215
     *
216
     * @param string $source Language code
217
     *
218
     * @return TranslateClient
219
     */
220
    public function setSource($source = null)
221
    {
222
        $this->sourceLanguage = is_null($source) ? 'auto' : $source;
223
224
        return $this;
225
    }
226
227
    /**
228
     * Set translation language we are transleting to.
229
     *
230
     * @param string $target Language code
231
     *
232
     * @return TranslateClient
233
     */
234
    public function setTarget($target)
235
    {
236
        $this->targetLanguage = $target;
237
238
        return $this;
239
    }
240
241
    /**
242
     * Set guzzleHttp client options.
243
     *
244
     * @param array $options guzzleHttp client options.
245
     *
246
     * @return TranslateClient
247
     */
248
    public function setHttpOption(array $options)
249
    {
250
        $this->httpOptions = $options;
251
252
        return $this;
253
    }
254
255
    /**
256
     * Get response array.
257
     *
258
     * @param string|array $data String or array of strings to translate
259
     *
260
     * @throws InvalidArgumentException If the provided argument is not of type 'string'
261
     * @throws ErrorException           If the HTTP request fails
262
     * @throws UnexpectedValueException If received data cannot be decoded
263
     *
264
     * @return array Response
265
     */
266
    private function getResponse($data)
267
    {
268
        if (!is_string($data) && !is_array($data)) {
269
            throw new InvalidArgumentException('Invalid argument provided');
270
        }
271
272
        $tokenData = is_array($data) ? implode('', $data) : $data;
273
274
        $queryArray = array_merge($this->urlParams, [
275
            'text' => $data,
276
            'sl'   => $this->sourceLanguage,
277
            'tl'   => $this->targetLanguage,
278
            'tk'   => $this->tokenProvider->generateToken($this->sourceLanguage, $this->targetLanguage, $tokenData),
279
        ]);
280
281
        $queryUrl = preg_replace('/%5B(?:[0-9]|[1-9][0-9]+)%5D=/', '=', http_build_query($queryArray));
282
283
        try {
284
            $response = $this->httpClient->post($this->urlBase, ['body' => $queryUrl] + $this->httpOptions);
285
        } catch (GuzzleRequestException $e) {
286
            throw new ErrorException($e->getMessage());
287
        }
288
289
        $body = $response->getBody(); // Get response body
290
291
        // Modify body to avoid json errors
292
        $bodyJson = preg_replace(array_keys($this->resultRegexes), array_values($this->resultRegexes), $body);
293
294
        // Decode JSON data
295
        if (($bodyArray = json_decode($bodyJson, true)) === null) {
296
            throw new UnexpectedValueException('Data cannot be decoded or it\'s deeper than the recursion limit');
297
        }
298
299
        return $bodyArray;
300
    }
301
302
    /**
303
     * Translate text.
304
     *
305
     * This can be called from instance method translate() using __call() magic method.
306
     * Use $instance->translate($string) instead.
307
     *
308
     * @param string|array $data Text or array of texts to translate
309
     *
310
     * @throws InvalidArgumentException If the provided argument is not of type 'string'
311
     * @throws ErrorException           If the HTTP request fails
312
     * @throws UnexpectedValueException If received data cannot be decoded
313
     *
314
     * @return string|bool Translated text
315
     */
316
    private function instanceTranslate($data)
317
    {
318
        // Whether or not is the data an array
319
        $isArray = is_array($data);
320
321
        // Rethrow exceptions
322
        try {
323
            $responseArray = $this->getResponse($data);
324
        } catch (Exception $e) {
325
            throw $e;
326
        }
327
328
        // if response in text and the content has zero the empty returns true, lets check
329
        // if response is string and not empty and create array for further logic
330
        if (is_string($responseArray) && $responseArray != '') {
331
            $responseArray = [$responseArray];
332
        }
333
334
        // Check if translation exists
335
        if (!isset($responseArray[0]) || empty($responseArray[0])) {
336
            return false;
337
        }
338
339
        // Detect languages
340
        $detectedLanguages = [];
341
342
        // the response contains only single translation, dont create loop that will end with
343
        // invalide foreach and warning
344
        if ($isArray || !is_string($responseArray)) {
345
            $responseArrayForLanguages = ($isArray) ? $responseArray[0] : [$responseArray];
346
            foreach ($responseArrayForLanguages as $itemArray) {
347
                foreach ($itemArray as $item) {
348
                    if (is_string($item)) {
349
                        $detectedLanguages[] = $item;
350
                    }
351
                }
352
            }
353
        }
354
355
        // Another case of detected language
356
        if (isset($responseArray[count($responseArray) - 2][0][0])) {
357
            $detectedLanguages[] = $responseArray[count($responseArray) - 2][0][0];
358
        }
359
360
        // Set initial detected language to null
361
        $this::$lastDetectedSource = false;
362
363
        // Iterate and set last detected language
364
        foreach ($detectedLanguages as $lang) {
365
            if ($this->isValidLocale($lang)) {
366
                $this::$lastDetectedSource = $lang;
367
                break;
368
            }
369
        }
370
371
        // Reduce array to generate translated sentenece
372
        if ($isArray) {
373
            $carry = [];
374
            foreach ($responseArray[0] as $item) {
375
                $carry[] = $item[0][0][0];
376
            }
377
378
            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...
379
        }
380
        // the response can be sometimes an translated string.
381
        elseif (is_string($responseArray)) {
382
            return $responseArray;
383
        } else {
384
            if (is_array($responseArray[0])) {
385
                return array_reduce($responseArray[0], function ($carry, $item) {
386
                    $carry .= $item[0];
387
388
                    return $carry;
389
                });
390
            } else {
391
                return $responseArray[0];
392
            }
393
        }
394
    }
395
396
    /**
397
     * Translate text statically.
398
     *
399
     * This can be called from static method translate() using __callStatic() magic method.
400
     * Use TranslateClient::translate($source, $target, $string) instead.
401
     *
402
     * @param string $source Source language
403
     * @param string $target Target language
404
     * @param string $string Text to translate
405
     *
406
     * @throws InvalidArgumentException If the provided argument is not of type 'string'
407
     * @throws ErrorException           If the HTTP request fails
408
     * @throws UnexpectedValueException If received data cannot be decoded
409
     *
410
     * @return string|bool Translated text
411
     */
412
    private static function staticTranslate($source, $target, $string)
413
    {
414
        self::checkStaticInstance();
415
        try {
416
            $result = self::$staticInstance
417
                ->setSource($source)
418
                ->setTarget($target)
419
                ->translate($string);
420
        } catch (Exception $e) {
421
            throw $e;
422
        }
423
424
        return $result;
425
    }
426
427
    /**
428
     * Get last detected language.
429
     *
430
     * @return string|bool Last detected language or boolean FALSE
431
     */
432
    private static function staticGetLastDetectedSource()
433
    {
434
        return self::$lastDetectedSource;
435
    }
436
437
    /**
438
     * Check if given locale is valid.
439
     *
440
     * @param string $lang Langauge code to verify
441
     *
442
     * @return bool
443
     */
444
    private function isValidLocale($lang)
445
    {
446
        return (bool) preg_match('/^([a-z]{2})(-[A-Z]{2})?$/', $lang);
447
    }
448
}
449