Completed
Push — master ( 09a9f8...98ccd3 )
by Enrico
13s queued 11s
created

SmsDev   A

Complexity

Total Complexity 38

Size/Duplication

Total Lines 430
Duplicated Lines 0 %

Test Coverage

Coverage 97.6%

Importance

Changes 3
Bugs 1 Features 1
Metric Value
eloc 105
c 3
b 1
f 1
dl 0
loc 430
ccs 122
cts 125
cp 0.976
rs 9.36
wmc 38

18 Methods

Rating   Name   Duplication   Size   Complexity  
A dateTo() 0 3 1
A isUnread() 0 5 1
A getResult() 0 3 1
A dateFrom() 0 3 1
A dateBetween() 0 3 1
A setFilter() 0 7 1
A getBalance() 0 21 2
B send() 0 41 6
A setNumberValidation() 0 3 1
A __construct() 0 5 1
A byId() 0 7 2
A fetch() 0 25 3
A setDateFormat() 0 5 1
A parsedMessages() 0 26 5
A makeRequest() 0 21 4
A getGuzzleClient() 0 3 1
A validatePhoneNumber() 0 20 4
A parseDate() 0 13 2
1
<?php
2
3
namespace enricodias;
4
5
use GuzzleHttp\Client;
6
use GuzzleHttp\Psr7\Request;
7
8
/**
9
 * SmsDev
10
 * 
11
 * Send and receive SMS using SmsDev.com.br
12
 * 
13
 * @see https://www.smsdev.com.br/ SMSDev API.
14
 * 
15
 * @author Enrico Dias <[email protected]>
16
 */
17
class SmsDev
18
{
19
    /**
20
     * API url.
21
     *
22
     * @var string
23
     */
24
    private $_apiUrl = 'https://api.smsdev.com.br/v1';
25
26
    /**
27
     * API key.
28
     *
29
     * @var string
30
     */
31
    private $_apiKey = '';
32
33
    /**
34
     * Whether or not to validate phone numbers locally before sending.
35
     *
36
     * @var bool
37
     */
38
    private $_numberValidation = true;
39
40
    /**
41
     * API timezone.
42
     *
43
     * @object \DateTimeZone
44
     */
45
    private $_apiTimeZone;
46
47
    /**
48
     * Date format to be used in all date functions.
49
     *
50
     * @var string
51
     */
52
    private $_dateFormat = 'U';
53
54
    /**
55
     * Query string to be sent to the API as a search filter.
56
     *
57
     * The default 'status' = 1 will return all received messages.
58
     * 
59
     * @var array
60
     */
61
    private $_query = [
62
        'status' => 1
63
    ];
64
65
    /**
66
     * Raw API response/
67
     *
68
     * @var array
69
     */
70
    private $_result = [];
71
72
    /**
73
     * Creates a new SmsDev instance with an API key and sets the default API timezone.
74
     *
75
     * @param string $apiKey
76
     */
77 43
    public function __construct($apiKey = '')
78
    {
79 43
        $this->_apiKey = $apiKey;
80
81 43
        $this->_apiTimeZone = new \DateTimeZone('America/Sao_Paulo');
82 43
    }
83
84
    /**
85
     * Send an SMS message.
86
     * 
87
     * This method does not guarantee that the recipient received the massage since the message delivery is async.
88
     *
89
     * @param int $number Recipient's number.
90
     * @param string $message SMS message.
91
     * @return bool true if the API accepted the request.
92
     */
93 28
    public function send($number, $message)
94
    {
95 28
        $this->_result = [];
96
97 28
        if ($this->_numberValidation === true) {
98
            
99
            try {
100
101 20
                $number = $this->validatePhoneNumber($number);
102
103 20
            } catch (\Exception $e) {
104
105 18
                return false;
106
107
            } catch (\Throwable $e) {
108
109
                return false;
110
111
            }
112
113 2
        }
114
        
115 10
        $request = new Request(
116 10
            'POST',
117 10
            $this->_apiUrl.'/send',
118
            [
119 10
                'Accept' => 'application/json',
120 10
            ],
121 10
            json_encode([
122 10
                'key'    => $this->_apiKey,
123 10
                'type'   => 9,
124 10
                'number' => $number,
125 10
                'msg'    => $message,
126 10
            ])
127 10
        );
128
129 10
        if ($this->makeRequest($request) === false) return false;
130
131 9
        if ($this->_result['situacao'] !== 'OK') return false;
132
133 5
        return true;
134
    }
135
136
    /**
137
     * Enables or disables the phone number validation
138
     *
139
     * @param bool $validate Whether or not to validate phone numbers.
140
     * @return void
141
     */
142 8
    public function setNumberValidation($validate = true)
143
    {
144 8
        $this->_numberValidation = (bool) $validate;
145 8
    }
146
147
    /**
148
     * Sets the date format to be used in all date functions.
149
     *
150
     * @param string $dateFormat A valid date format (ex: Y-m-d).
151
     * @return SmsDev Return itself for chaining.
152
     */
153 9
    public function setDateFormat($dateFormat)
154
    {
155 9
        $this->_dateFormat = $dateFormat;
156
157 9
        return $this;
158
    }
159
160
    /**
161
     * Resets the search filter.
162
     *
163
     * @return SmsDev Return itself for chaining.
164
     */
165 11
    public function setFilter()
166
    {
167 11
        $this->_query = [
168
            'status' => 1
169 11
        ];
170
171 11
        return $this;
172
    }
173
174
    /**
175
     * Sets the search filter to return unread messages only.
176
     *
177
     * @return SmsDev Return itself for chaining.
178
     */
179 3
    public function isUnread()
180
    {
181 3
        $this->_query['status'] = 0;
182
183 3
        return $this;
184
    }
185
186
    /**
187
     * Sets the search filter to return a message with a specific id.
188
     *
189
     * @param int $id Message id.
190
     * @return SmsDev Return itself for chaining.
191
     */
192 3
    public function byId($id)
193
    {
194 3
        $id = intval($id);
195
196 3
        if ($id > 0) $this->_query['id'] = $id;
197
198 3
        return $this;
199
    }
200
201
    /**
202
     * Sets the search filter to return messages older than a specific date.
203
     *
204
     * @param string $date A valid date.
205
     * @return SmsDev Return itself for chaining.
206
     */
207 5
    public function dateFrom($date)
208
    {
209 5
        return $this->parseDate('date_from', $date);
210
    }
211
212
    /**
213
     * Sets the search filter to return messages newer than a specific date.
214
     *
215
     * @param string $date A valid date.
216
     * @return SmsDev Return itself for chaining.
217
     */
218 4
    public function dateTo($date)
219
    {
220 4
        return $this->parseDate('date_to', $date);
221
    }
222
223
    /**
224
     * Sets the search filter to return messages between a specific date interval.
225
     *
226
     * @param string $dateFrom Minimum date.
227
     * @param string $dateTo Maximum date.
228
     * @return SmsDev Return itself for chaining.
229
     */
230 2
    public function dateBetween($dateFrom, $dateTo)
231
    {
232 2
        return $this->dateFrom($dateFrom)->dateTo($dateTo);
233
    }
234
235
    /**
236
     * Query the API for received messages using search filters.
237
     * 
238
     * @see SmsDev::$_query Search filters.
239
     * @see SmsDev::$_result API response.
240
     *
241
     * @return bool True if the request if the API response is valid.
242
     */
243 13
    public function fetch()
244
    {
245 13
        $this->_result = [];
246
247 13
        $this->_query['key'] = $this->_apiKey;
248
249 13
        $request = new Request(
250 13
            'GET',
251 13
            $this->_apiUrl.'/inbox',
252
            [
253 13
                'Accept' => 'application/json',
254 13
            ],
255 13
            json_encode(
256 13
                $this->_query
257 13
            )
258 13
        );
259
260 13
        if ($this->makeRequest($request) === false) return false;
261
262
        // resets the filters
263 4
        $this->setFilter();
264
265 4
        if (is_array($this->_result)) return true;
266
267
        return false;
268
    }
269
270
    /**
271
     * Parse the received messages in a more useful format with the fields date, number and message.
272
     * 
273
     * The dates received by the API are converted to SmsDev::$_dateFormat.
274
     *
275
     * @see SmsDev::$_dateFormat Date format to be used in all date functions.
276
     * 
277
     * @return array List of received messages.
278
     */
279 5
    public function parsedMessages()
280
    {
281 5
        $localTimeZone = new \DateTimeZone(date_default_timezone_get());
282
283 5
        $messages = [];
284
285 5
        foreach ($this->_result as $key => $result) {
286
287 3
            if (is_array($result) === false) continue;
288
289 3
            if (is_array($result) === false || array_key_exists('id_sms_read', $result) === false) continue;
290
291 2
            $id = $result['id_sms_read'];
292 2
            $date = \DateTime::createFromFormat('d/m/Y H:i:s', $result['data_read'], $this->_apiTimeZone);
293
294 2
            $date->setTimezone($localTimeZone);
295
296 2
            $messages[$id] = [
297 2
                'date'    => $date->format($this->_dateFormat),
298 2
                'number'  => $result['telefone'],
299 2
                'message' => $result['descricao'],
300
            ];
301
302 5
        }
303
304 5
        return $messages;
305
    }
306
307
    /**
308
     * Get the current balance/credits.
309
     *
310
     * @return int Current balance in BRL cents.
311
     */
312 3
    public function getBalance()
313
    {
314 3
        $this->_result = [];
315
316 3
        $request = new Request(
317 3
            'GET',
318 3
            $this->_apiUrl.'/balance',
319
            [
320 3
                'Accept' => 'application/json',
321 3
            ],
322 3
            json_encode([
323 3
                'key'    => $this->_apiKey,
324 3
                'action' => 'saldo',
325 3
            ])
326 3
        );
327
328 3
        $this->makeRequest($request);
329
330 3
        if (array_key_exists('saldo_sms', $this->_result) === false) return 0;
331
332 1
        return (int) $this->_result['saldo_sms'];
333
    }
334
335
    /**
336
     * Get the raw API response from the last response received.
337
     *
338
     * @see SmsDev::$_result Raw API response.
339
     * 
340
     * @return array Raw API response.
341
     */
342 2
    public function getResult()
343
    {
344 2
        return $this->_result;
345
    }
346
347
    /**
348
     * Verifies if a phone number is valid
349
     *
350
     * @see https://github.com/giggsey/libphonenumber-for-php libphonenumber for PHP repository.
351
     * 
352
     * @param int $number Phone number.
353
     * @return int A valid mobile phone number.
354
     * 
355
     * @throws \libphonenumber\NumberParseException If the number is not valid.
356
     * @throws \Exception If the number is not a valid brazilian mobile number.
357
     */
358 20
    private function validatePhoneNumber($number)
359
    {
360 20
        if (class_exists('\libphonenumber\PhoneNumberUtil')) {
361
362 20
            $phoneNumberUtil = /** @scrutinizer ignore-call */ \libphonenumber\PhoneNumberUtil::getInstance();
1 ignored issue
show
Bug introduced by
The type libphonenumber\PhoneNumberUtil was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
363 20
            $mobilePhoneNumber = /** @scrutinizer ignore-call */ \libphonenumber\PhoneNumberType::MOBILE;
1 ignored issue
show
Bug introduced by
The type libphonenumber\PhoneNumberType was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
364
365 20
            $phoneNumberObject = $phoneNumberUtil->parse($number, 'BR');
366
367 15
            if ($phoneNumberUtil->isValidNumber($phoneNumberObject) === false || $phoneNumberUtil->getNumberType($phoneNumberObject) !== $mobilePhoneNumber) {
368
369 13
                throw new \Exception('Invalid phone number.');
370
371
            }
372
373 2
            $number = $phoneNumberObject->getCountryCode().$phoneNumberObject->getNationalNumber();
374
375 2
        }
376
377 2
        return (int) $number;
378
    }
379
380
    /**
381
     * Convert a date to format supported by the API.
382
     * 
383
     * The API requires the date format d/m/Y, but in this class any valid date format is supported.
384
     * Since the API is always using the timezone America/Sao_Paulo, this function must also do timezone conversions.
385
     *
386
     * @see SmsDev::$_dateFormat Date format to be used in all date functions.
387
     * 
388
     * @param string $key The filter key to be set as a search filter.
389
     * @param string $date A valid date format.
390
     * @return SmsDev Return itself for chaining.
391
     */
392 5
    private function parseDate($key, $date)
393
    {
394 5
        $parsedDate = \DateTime::createFromFormat($this->_dateFormat, $date);
395
396 5
        if ($parsedDate !== false) {
397
            
398 5
            $parsedDate->setTimezone($this->_apiTimeZone);
399
400 5
            $this->_query[$key] = $parsedDate->format('d/m/Y');
401
402 5
        }
403
404 5
        return $this;
405
    }
406
407
    /**
408
     * Sends a request to the smsdev.com.br API.
409
     *
410
     * @param \GuzzleHttp\Psr7\Request $request Request object.
411
     * @return bool True if the API response is valid.
412
     */
413 25
    private function makeRequest($request)
414
    {
415 25
        $client = $this->getGuzzleClient();
416
417
        try {
418
419 25
            $response = $client->send($request);
420
421 25
        } catch (\Exception $e) {
422
            
423 1
            return false;
424
            
425
        }
426
427 25
        $response = json_decode($response->getBody(), true);
428
        
429 25
        if (json_last_error() != JSON_ERROR_NONE || is_array($response) === false) return false;
430
431 14
        $this->_result = $response;
432
433 14
        return true;
434
    }
435
436
    /**
437
     * Creates GuzzleHttp\Client to be used in API requests.
438
     * This method is needed to test API calls in unit tests.
439
     *
440
     * @return object GuzzleHttp\Client instance.
441
     * 
442
     * @codeCoverageIgnore
443
     */
444
    protected function getGuzzleClient()
445
    {
446
        return new Client();
447
    }
448
}