Completed
Pull Request — master (#79)
by
unknown
01:48
created

TranslateClient::setTarget()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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