Completed
Pull Request — master (#44)
by Denis
02:35
created

TranslateClient::instanceTranslate()   D

Complexity

Conditions 15
Paths 242

Size

Total Lines 65
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 15
Bugs 5 Features 3
Metric Value
c 15
b 5
f 3
dl 0
loc 65
rs 4.5728
cc 15
eloc 33
nc 242
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
        // Check if translation exists
305
        if (!isset($responseArray[0]) || empty($responseArray[0])) {
306
            return false;
307
        }
308
309
        // Detect languages
310
        $detectedLanguages = [];
311
        $responseArrayForLanguages = ($isArray) ? $responseArray[0] : [$responseArray];
312
        foreach ($responseArrayForLanguages as $itemArray) {
313
            if (is_array($itemArray)) {
314
                foreach ($itemArray as $item) {
315
                    if (is_string($item)) {
316
                        $detectedLanguages[] = $item;
317
                    }
318
                }
319
            }
320
        }
321
322
        // Another case of detected language
323
        if (isset($responseArray[count($responseArray) - 2][0][0])) {
324
            $detectedLanguages[] = $responseArray[count($responseArray) - 2][0][0];
325
        }
326
327
        // Set initial detected language to null
328
        $this::$lastDetectedSource = false;
329
330
        // Iterate and set last detected language
331
        foreach ($detectedLanguages as $lang) {
332
            if ($this->isValidLocale($lang)) {
333
                $this::$lastDetectedSource = $lang;
334
                break;
335
            }
336
        }
337
338
        // Reduce array to generate translated sentenece
339
        if ($isArray) {
340
            $carry = [];
341
            foreach ($responseArray[0] as $item) {
342
                $carry[] = $item[0][0][0];
343
            }
344
345
            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...
346
        } else {
347
            if (is_string($responseArray)){
348
                return $responseArray;
349
            }
350
            return array_reduce($responseArray[0], function($carry, $item) {
351
                $carry .= $item[0];
352
353
                return $carry;
354
            });
355
        }
356
    }
357
358
    /**
359
     * Translate text statically.
360
     *
361
     * This can be called from static method translate() using __callStatic() magic method.
362
     * Use TranslateClient::translate($source, $target, $string) instead.
363
     *
364
     * @param string $source Source language
365
     * @param string $target Target language
366
     * @param string $string Text to translate
367
     *
368
     * @throws InvalidArgumentException If the provided argument is not of type 'string'
369
     * @throws ErrorException           If the HTTP request fails
370
     * @throws UnexpectedValueException If received data cannot be decoded
371
     *
372
     * @return string|bool Translated text
373
     */
374
    private static function staticTranslate($source, $target, $string)
375
    {
376
        self::checkStaticInstance();
377
        try {
378
            $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...
379
                ->setSource($source)
380
                ->setTarget($target)
381
                ->translate($string);
382
        } catch (Exception $e) {
383
            throw $e;
384
        }
385
386
        return $result;
387
    }
388
389
    /**
390
     * Get last detected language.
391
     *
392
     * @return string|bool Last detected language or boolean FALSE
393
     */
394
    private static function staticGetLastDetectedSource()
395
    {
396
        return self::$lastDetectedSource;
397
    }
398
399
    /**
400
     * Check if given locale is valid.
401
     *
402
     * @param string $lang Langauge code to verify
403
     *
404
     * @return bool
405
     */
406
    private function isValidLocale($lang)
407
    {
408
        return (bool) preg_match('/^([a-z]{2})(-[A-Z]{2})?$/', $lang);
409
    }
410
}
411