Completed
Pull Request — master (#51)
by Levan
02:17
created

TranslateClient::getResponse()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 35
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 13
Bugs 3 Features 1
Metric Value
c 13
b 3
f 1
dl 0
loc 35
rs 8.439
cc 6
eloc 19
nc 7
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
class TranslateClient
26
{
27
    /**
28
     * @var TranslateClient Because nobody cares about singletons
29
     */
30
    private static $staticInstance;
31
32
    /**
33
     * @var \GuzzleHttp\Client HTTP Client
34
     */
35
    private $httpClient;
36
37
    /**
38
     * @var string Source language - from where the string should be translated
39
     */
40
    private $sourceLanguage;
41
42
    /**
43
     * @var string Target language - to which language string should be translated
44
     */
45
    private $targetLanguage;
46
47
    /**
48
     * @var string|bool Last detected source language
49
     */
50
    private static $lastDetectedSource;
51
52
    /**
53
     * @var string Google Translate URL base
54
     */
55
    private $urlBase = 'http://translate.google.com/translate_a/t';
56
57
    /**
58
     * @var array URL Parameters
59
     */
60
    private $urlParams = [
61
        'client'   => 't',
62
        'hl'       => 'en',
63
        'sl'       => null, // Source language
64
        'tl'       => null, // Target language
65
        'text'     => null, // String to translate
66
        'ie'       => 'UTF-8', // Input encoding
67
        'oe'       => 'UTF-8', // Output encoding
68
        'multires' => 1,
69
        'otf'      => 0,
70
        'pc'       => 1,
71
        'trs'      => 1,
72
        'ssel'     => 0,
73
        'tsel'     => 0,
74
        'sc'       => 1,
75
        'tk'       => null,
76
    ];
77
78
    /**
79
     * @var array Regex key-value patterns to replace on response data
80
     */
81
    private $resultRegexes = [
82
        '/,+/'  => ',',
83
        '/\[,/' => '[',
84
    ];
85
86
    /**
87
     * @var TokenProviderInterface
88
     */
89
    private $tokenProvider;
90
91
    /**
92
     * @var string Default token generator class name
93
     */
94
    private $defaultTokenProvider = GoogleTokenGenerator::class;
95
96
    /**
97
     * Class constructor.
98
     *
99
     * For more information about HTTP client configuration options, visit
100
     * "Creating a client" section of GuzzleHttp docs.
101
     * 5.x - http://guzzle.readthedocs.org/en/5.3/clients.html#creating-a-client
102
     *
103
     * @param string $source  Source language (Optional)
104
     * @param string $target  Target language (Optional)
105
     * @param array  $options Associative array of http client configuration options (Optional)
106
     *
107
     * @throws Exception If token provider does not implement TokenProviderInterface
108
     */
109
    public function __construct($source = null, $target = 'en', $options = [], TokenProviderInterface $tokener = null)
110
    {
111
        $this->httpClient = new GuzzleHttpClient($options); // Create HTTP client
112
        $this->setSource($source)->setTarget($target); // Set languages
113
        $this::$lastDetectedSource = false;
114
115
        if (!isset($tokener)) {
116
            $tokener = $this->defaultTokenProvider;
117
        }
118
119
        $tokenProviderReflection = new ReflectionClass($tokener);
120
121
        if ($tokenProviderReflection->implementsInterface(TokenProviderInterface::class)) {
122
            $this->tokenProvider = $tokenProviderReflection->newInstance();
123
        } else {
124
            throw new Exception('Token provider should implement TokenProviderInterface');
125
        }
126
    }
127
128
    /**
129
     * Override translate method for static call.
130
     *
131
     * @throws BadMethodCallException   If calling nonexistent method
132
     * @throws InvalidArgumentException If parameters are passed incorrectly
133
     * @throws InvalidArgumentException If the provided argument is not of type 'string'
134
     * @throws ErrorException           If the HTTP request fails
135
     * @throws UnexpectedValueException If received data cannot be decoded
136
     */
137
    public static function __callStatic($name, $args)
138
    {
139
        switch ($name) {
140
            case 'translate':
141
                if (count($args) < 3) {
142
                    throw new InvalidArgumentException('Expecting 3 parameters');
143
                }
144
                try {
145
                    $result = self::staticTranslate($args[0], $args[1], $args[2]);
146
                } catch (Exception $e) {
147
                    throw $e;
148
                }
149
150
                return $result;
151
            case 'getLastDetectedSource':
152
                return self::staticGetLastDetectedSource();
153
            default:
154
                throw new BadMethodCallException("Method [{$name}] does not exist");
155
        }
156
    }
157
158
    /**
159
     * Override translate method for instance call.
160
     *
161
     * @throws BadMethodCallException   If calling nonexistent method
162
     * @throws InvalidArgumentException If parameters are passed incorrectly
163
     * @throws InvalidArgumentException If the provided argument is not of type 'string'
164
     * @throws ErrorException           If the HTTP request fails
165
     * @throws UnexpectedValueException If received data cannot be decoded
166
     */
167
    public function __call($name, $args)
168
    {
169
        switch ($name) {
170
            case 'translate':
171
                if (count($args) < 1) {
172
                    throw new InvalidArgumentException('Expecting 1 parameter');
173
                }
174
                try {
175
                    $result = $this->instanceTranslate($args[0]);
176
                } catch (Exception $e) {
177
                    throw $e;
178
                }
179
180
                return $result;
181
            case 'getLastDetectedSource':
182
                return $this::staticGetLastDetectedSource();
183
            case 'getResponse':
184
                // getResponse is available for instanse calls only.
185
                return $this->getResponse($args[0]);
186
            default:
187
                throw new BadMethodCallException("Method [{$name}] does not exist");
188
        }
189
    }
190
191
    /**
192
     * Check if static instance exists and instantiate if not.
193
     *
194
     * @return void
195
     */
196
    private static function checkStaticInstance()
197
    {
198
        if (!isset(self::$staticInstance)) {
199
            self::$staticInstance = new self();
200
        }
201
    }
202
203
    /**
204
     * Set source language we are transleting from.
205
     *
206
     * @param string $source Language code
207
     *
208
     * @return TranslateClient
209
     */
210
    public function setSource($source = null)
211
    {
212
        $this->sourceLanguage = is_null($source) ? 'auto' : $source;
213
214
        return $this;
215
    }
216
217
    /**
218
     * Set translation language we are transleting to.
219
     *
220
     * @param string $target Language code
221
     *
222
     * @return TranslateClient
223
     */
224
    public function setTarget($target)
225
    {
226
        $this->targetLanguage = $target;
227
228
        return $this;
229
    }
230
231
    /**
232
     * Get response array.
233
     *
234
     * @param string|array $data String or array of strings to translate
235
     *
236
     * @throws InvalidArgumentException If the provided argument is not of type 'string'
237
     * @throws ErrorException           If the HTTP request fails
238
     * @throws UnexpectedValueException If received data cannot be decoded
239
     *
240
     * @return array Response
241
     */
242
    private function getResponse($data)
243
    {
244
        if (!is_string($data) && !is_array($data)) {
245
            throw new InvalidArgumentException('Invalid argument provided');
246
        }
247
248
        $tokenData = is_array($data) ? implode('', $data) : $data;
249
250
        $queryArray = array_merge($this->urlParams, [
251
            'text' => $data,
252
            'sl'   => $this->sourceLanguage,
253
            'tl'   => $this->targetLanguage,
254
            'tk'   => $this->tokenProvider->generateToken($this->sourceLanguage, $this->targetLanguage, $tokenData),
255
        ]);
256
257
        $queryUrl = preg_replace('/%5B(?:[0-9]|[1-9][0-9]+)%5D=/', '=', http_build_query($queryArray));
258
259
        try {
260
            $response = $this->httpClient->post($this->urlBase, ['body' => $queryUrl]);
261
        } catch (GuzzleRequestException $e) {
262
            throw new ErrorException($e->getMessage());
263
        }
264
265
        $body = $response->getBody(); // Get response body
266
267
        // Modify body to avoid json errors
268
        $bodyJson = preg_replace(array_keys($this->resultRegexes), array_values($this->resultRegexes), $body);
269
270
        // Decode JSON data
271
        if (($bodyArray = json_decode($bodyJson, true)) === null) {
272
            throw new UnexpectedValueException('Data cannot be decoded or it\'s deeper than the recursion limit');
273
        }
274
275
        return $bodyArray;
276
    }
277
278
    /**
279
     * Translate text.
280
     *
281
     * This can be called from instance method translate() using __call() magic method.
282
     * Use $instance->translate($string) instead.
283
     *
284
     * @param string|array $data Text or array of texts to translate
285
     *
286
     * @throws InvalidArgumentException If the provided argument is not of type 'string'
287
     * @throws ErrorException           If the HTTP request fails
288
     * @throws UnexpectedValueException If received data cannot be decoded
289
     *
290
     * @return string|bool Translated text
291
     */
292
    private function instanceTranslate($data)
293
    {
294
        // Whether or not is the data an array
295
        $isArray = is_array($data);
296
297
        // Rethrow exceptions
298
        try {
299
            $responseArray = $this->getResponse($data);
300
        } catch (Exception $e) {
301
            throw $e;
302
        }
303
304
        // if response in text and the content has zero the empty returns true, lets check
305
        // if response is string and not empty and create array for further logic
306
        if (is_string($responseArray) && $responseArray != '') {
307
            $responseArray = [$responseArray];
308
        }
309
310
        // Check if translation exists
311
        if (!isset($responseArray[0]) || empty($responseArray[0])) {
312
            return false;
313
        }
314
315
        // Detect languages
316
        $detectedLanguages = [];
317
318
        // the response contains only single translation, dont create loop that will end with
319
        // invalide foreach and warning
320
        if ($isArray || !is_string($responseArray)) {
321
            $responseArrayForLanguages = ($isArray) ? $responseArray[0] : [$responseArray];
322
            foreach ($responseArrayForLanguages as $itemArray) {
323
                foreach ($itemArray as $item) {
324
                    if (is_string($item)) {
325
                        $detectedLanguages[] = $item;
326
                    }
327
                }
328
            }
329
        }
330
331
        // Another case of detected language
332
        if (isset($responseArray[count($responseArray) - 2][0][0])) {
333
            $detectedLanguages[] = $responseArray[count($responseArray) - 2][0][0];
334
        }
335
336
        // Set initial detected language to null
337
        $this::$lastDetectedSource = false;
338
339
        // Iterate and set last detected language
340
        foreach ($detectedLanguages as $lang) {
341
            if ($this->isValidLocale($lang)) {
342
                $this::$lastDetectedSource = $lang;
343
                break;
344
            }
345
        }
346
347
        // Reduce array to generate translated sentenece
348
        if ($isArray) {
349
            $carry = [];
350
            foreach ($responseArray[0] as $item) {
351
                $carry[] = $item[0][0][0];
352
            }
353
354
            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...
355
        }
356
        // the response can be sometimes an translated string.
357
        elseif (is_string($responseArray)) {
358
            return $responseArray;
359
        } else {
360
            if (is_array($responseArray[0])) {
361
                return array_reduce($responseArray[0], function ($carry, $item) {
362
                    $carry .= $item[0];
363
364
                    return $carry;
365
                });
366
            } else {
367
                return $responseArray[0];
368
            }
369
        }
370
    }
371
372
    /**
373
     * Translate text statically.
374
     *
375
     * This can be called from static method translate() using __callStatic() magic method.
376
     * Use TranslateClient::translate($source, $target, $string) instead.
377
     *
378
     * @param string $source Source language
379
     * @param string $target Target language
380
     * @param string $string Text to translate
381
     *
382
     * @throws InvalidArgumentException If the provided argument is not of type 'string'
383
     * @throws ErrorException           If the HTTP request fails
384
     * @throws UnexpectedValueException If received data cannot be decoded
385
     *
386
     * @return string|bool Translated text
387
     */
388
    private static function staticTranslate($source, $target, $string)
389
    {
390
        self::checkStaticInstance();
391
        try {
392
            $result = self::$staticInstance
0 ignored issues
show
Bug introduced by
The method translate() does not exist on Stichoza\GoogleTranslate\TranslateClient. Did you maybe mean instanceTranslate()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
393
                ->setSource($source)
394
                ->setTarget($target)
395
                ->translate($string);
396
        } catch (Exception $e) {
397
            throw $e;
398
        }
399
400
        return $result;
401
    }
402
403
    /**
404
     * Get last detected language.
405
     *
406
     * @return string|bool Last detected language or boolean FALSE
407
     */
408
    private static function staticGetLastDetectedSource()
409
    {
410
        return self::$lastDetectedSource;
411
    }
412
413
    /**
414
     * Check if given locale is valid.
415
     *
416
     * @param string $lang Langauge code to verify
417
     *
418
     * @return bool
419
     */
420
    private function isValidLocale($lang)
421
    {
422
        return (bool) preg_match('/^([a-z]{2})(-[A-Z]{2})?$/', $lang);
423
    }
424
}
425