├── CHANGELOG.md ├── composer.json ├── LICENSE └── src └── SmsDev.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file, in reverse chronological order by release. 4 | 5 | ## Unreleased 6 | 7 | #### Added 8 | - Add support for loading the API key from an environment variable 9 | 10 | ## 0.4 11 | 12 | _Released: 2022-06-01_ 13 | #### Added 14 | 15 | - Add a Changelog 16 | - Add a Code of Conduct 17 | - Add support for the 'refer' parameter 18 | - Add support for guzzle v7.4 19 | 20 | ## 0.3 21 | 22 | _Released: 2020-11-26_ 23 | 24 | #### Added 25 | 26 | - Optional phone number validation using [giggsey/libphonenumber-for-php](https://github.com/giggsey/libphonenumber-for-php) 27 | 28 | ## 0.2 29 | 30 | _Released: 2020-10-19_ 31 | 32 | #### Fixed 33 | 34 | - Fix namespaces 35 | - Fix request body in send, getBalance and fetch methods 36 | - Update API URL and endpoints 37 | 38 | ## 0.1 39 | 40 | _Released: 2020-01-19_ 41 | 42 | #### Added 43 | 44 | - First working version 45 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "enricodias/smsdev", 3 | "type": "library", 4 | "license": "MIT", 5 | "description": "Send and receive SMS using SmsDev.com.br", 6 | "keywords": ["smsdev", "sms"], 7 | "authors": [ 8 | { 9 | "name": "Enrico Dias", 10 | "email": "enrico@enricodias.com" 11 | } 12 | ], 13 | "config": { 14 | "sort-packages": true 15 | }, 16 | "require": { 17 | "php": "^5.6 || ^7.0 || ^8.0", 18 | "guzzlehttp/guzzle": "^6.4 || ^7.5" 19 | }, 20 | "suggest": { 21 | "giggsey/libphonenumber-for-php": "Allows phone number verification" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "enricodias\\": "src/" 26 | } 27 | }, 28 | "autoload-dev": { 29 | "psr-4": { 30 | "enricodias\\SmsDev\\Tests\\": "tests/" 31 | } 32 | }, 33 | "require-dev": { 34 | "phpunit/phpunit": "^5.7 || ^9.0", 35 | "scrutinizer/ocular": "1.7.*" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Enrico Dias 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/SmsDev.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class SmsDev 18 | { 19 | /** 20 | * @var string 21 | */ 22 | private $apiUrl = 'https://api.smsdev.com.br/v1'; 23 | 24 | /** 25 | * @var string 26 | */ 27 | private $apiKey = ''; 28 | 29 | /** 30 | * Whether or not to validate phone numbers locally before sending. 31 | * 32 | * @var bool 33 | */ 34 | private $numberValidation = true; 35 | 36 | /** 37 | * @var \DateTimeZone 38 | */ 39 | private $apiTimeZone; 40 | 41 | /** 42 | * Date format to be used in all date functions. 43 | * 44 | * @var string 45 | */ 46 | private $dateFormat = 'U'; 47 | 48 | /** 49 | * Query string to be sent to the API as a search filter. 50 | * 51 | * The default 'status' = 1 will return all received messages. 52 | * 53 | * @var array 54 | */ 55 | private $query = [ 56 | 'status' => 1 57 | ]; 58 | 59 | /** 60 | * Raw API response. 61 | * 62 | * @var array 63 | */ 64 | private $_result = []; 65 | 66 | /** 67 | * Creates a new SmsDev instance with an API key and sets the default API timezone. 68 | * 69 | * @param string $apiKey 70 | */ 71 | public function __construct($apiKey = '') 72 | { 73 | if ($apiKey === '' && \array_key_exists('SMSDEV_API_KEY', $_SERVER) === true) { 74 | $apiKey = $_SERVER['SMSDEV_API_KEY']; 75 | } 76 | 77 | $this->apiKey = $apiKey; 78 | 79 | $this->apiTimeZone = new \DateTimeZone('America/Sao_Paulo'); 80 | } 81 | 82 | /** 83 | * Send an SMS message. 84 | * 85 | * This method does not guarantee that the recipient received the massage since the message delivery is async. 86 | * 87 | * @param int $number 88 | * @param string $message 89 | * @param string $refer (optional) User reference for message identification. 90 | * 91 | * @return bool true if the API accepted the request. 92 | */ 93 | public function send($number, $message, $refer = null) 94 | { 95 | $this->_result = []; 96 | 97 | if ($this->numberValidation === true) { 98 | try { 99 | $number = $this->validatePhoneNumber($number); 100 | } catch (\Exception $e) { 101 | return false; 102 | } catch (\Throwable $e) { 103 | return false; 104 | } 105 | } 106 | 107 | $params = [ 108 | 'key' => $this->apiKey, 109 | 'type' => 9, 110 | 'number' => $number, 111 | 'msg' => $message, 112 | ]; 113 | 114 | if ($refer) $params['refer'] = $refer; 115 | 116 | $request = new Request( 117 | 'POST', 118 | $this->apiUrl.'/send', 119 | [ 120 | 'Accept' => 'application/json', 121 | ], 122 | \json_encode($params) 123 | ); 124 | 125 | if ($this->makeRequest($request) === false || $this->_result['situacao'] !== 'OK') { 126 | return false; 127 | } 128 | 129 | return true; 130 | } 131 | 132 | /** 133 | * Enables or disables the phone number validation. 134 | * 135 | * @param bool $shouldValidate 136 | * 137 | * @return void 138 | */ 139 | public function setNumberValidation($shouldValidate = true) 140 | { 141 | $this->numberValidation = (bool) $shouldValidate; 142 | } 143 | 144 | /** 145 | * Sets the date format to be used in all date functions. 146 | * 147 | * @param string $dateFormat A valid date format (ex: Y-m-d). 148 | * @return SmsDev 149 | */ 150 | public function setDateFormat($dateFormat) 151 | { 152 | $this->dateFormat = $dateFormat; 153 | 154 | return $this; 155 | } 156 | 157 | /** 158 | * Resets the search filter. 159 | * 160 | * @return SmsDev 161 | */ 162 | public function setFilter() 163 | { 164 | $this->query = [ 165 | 'status' => 1, 166 | ]; 167 | 168 | return $this; 169 | } 170 | 171 | /** 172 | * Sets the search filter to return unread messages only. 173 | * 174 | * @return SmsDev 175 | */ 176 | public function isUnread() 177 | { 178 | $this->query['status'] = 0; 179 | 180 | return $this; 181 | } 182 | 183 | /** 184 | * Sets the search filter to return a message with a specific id. 185 | * 186 | * @param int $id 187 | * 188 | * @return SmsDev 189 | */ 190 | public function byId($id) 191 | { 192 | $id = \intval($id); 193 | 194 | if ($id > 0) { 195 | $this->query['id'] = $id; 196 | } 197 | 198 | return $this; 199 | } 200 | 201 | /** 202 | * Sets the search filter to return messages older than a specific date. 203 | * 204 | * @param string $date 205 | * 206 | * @return SmsDev 207 | */ 208 | public function dateFrom($date) 209 | { 210 | return $this->parseDate('date_from', $date); 211 | } 212 | 213 | /** 214 | * Sets the search filter to return messages newer than a specific date. 215 | * 216 | * @param string $date 217 | * 218 | * @return SmsDev 219 | */ 220 | public function dateTo($date) 221 | { 222 | return $this->parseDate('date_to', $date); 223 | } 224 | 225 | /** 226 | * Sets the search filter to return messages between a specific date interval. 227 | * 228 | * @param string $dateFrom 229 | * @param string $dateTo 230 | * 231 | * @return SmsDev 232 | */ 233 | public function dateBetween($dateFrom, $dateTo) 234 | { 235 | return $this->dateFrom($dateFrom)->dateTo($dateTo); 236 | } 237 | 238 | /** 239 | * Query the API for received messages using search filters. 240 | * 241 | * @see SmsDev::$query Search filters. 242 | * @see SmsDev::$_result API response. 243 | * 244 | * @return bool True if the request was successful. 245 | */ 246 | public function fetch() 247 | { 248 | $this->_result = []; 249 | 250 | $this->query['key'] = $this->apiKey; 251 | 252 | $request = new Request( 253 | 'GET', 254 | $this->apiUrl.'/inbox', 255 | [ 256 | 'Accept' => 'application/json', 257 | ], 258 | \json_encode( 259 | $this->query 260 | ) 261 | ); 262 | 263 | if ($this->makeRequest($request) === false) { 264 | return false; 265 | } 266 | 267 | // resets the filters 268 | $this->setFilter(); 269 | 270 | if (\is_array($this->_result) === true) { 271 | return true; 272 | } 273 | 274 | return false; 275 | } 276 | 277 | /** 278 | * Parse the received messages in a more useful format with the fields date, number and message. 279 | * 280 | * The dates received by the API are converted to SmsDev::$dateFormat. 281 | * 282 | * @see SmsDev::$dateFormat Date format to be used in all date functions. 283 | * 284 | * @return array List of received messages. 285 | */ 286 | public function parsedMessages() 287 | { 288 | $localTimeZone = new \DateTimeZone(\date_default_timezone_get()); 289 | 290 | $messages = []; 291 | 292 | foreach ($this->_result as $key => $result) { 293 | if (\is_array($result) === false || \array_key_exists('id_sms_read', $result) === false) { 294 | continue; 295 | } 296 | 297 | $id = $result['id_sms_read']; 298 | $date = \DateTime::createFromFormat('d/m/Y H:i:s', $result['data_read'], $this->apiTimeZone); 299 | 300 | $date->setTimezone($localTimeZone); 301 | 302 | $messages[$id] = [ 303 | 'date' => $date->format($this->dateFormat), 304 | 'number' => $result['telefone'], 305 | 'message' => $result['descricao'], 306 | ]; 307 | } 308 | 309 | return $messages; 310 | } 311 | 312 | /** 313 | * Get the current balance/credits. 314 | * 315 | * @return int Current balance in BRL cents. 316 | */ 317 | public function getBalance() 318 | { 319 | $this->_result = []; 320 | 321 | $request = new Request( 322 | 'GET', 323 | $this->apiUrl.'/balance', 324 | [ 325 | 'Accept' => 'application/json', 326 | ], 327 | \json_encode([ 328 | 'key' => $this->apiKey, 329 | 'action' => 'saldo', 330 | ]) 331 | ); 332 | 333 | $this->makeRequest($request); 334 | 335 | if (\array_key_exists('saldo_sms', $this->_result) === false) { 336 | return 0; 337 | } 338 | 339 | return (int) $this->_result['saldo_sms']; 340 | } 341 | 342 | /** 343 | * Get the raw API response from the last response received. 344 | * 345 | * @see SmsDev::$_result Raw API response. 346 | * 347 | * @return array Raw API response. 348 | */ 349 | public function getResult() 350 | { 351 | return $this->_result; 352 | } 353 | 354 | /** 355 | * Verifies if a phone number is valid. 356 | * 357 | * @see https://github.com/giggsey/libphonenumber-for-php libphonenumber for PHP repository. 358 | * 359 | * @param int $number 360 | * 361 | * @return int A valid mobile phone number. 362 | * 363 | * @throws \libphonenumber\NumberParseException If the number is not valid. 364 | * @throws \Exception If the number is not a valid brazilian mobile number. 365 | */ 366 | private function validatePhoneNumber($number) 367 | { 368 | if (\class_exists('\libphonenumber\PhoneNumberUtil') === true) { 369 | $phoneNumberUtil = /** @scrutinizer ignore-call */ \libphonenumber\PhoneNumberUtil::getInstance(); 370 | $mobilePhoneNumber = /** @scrutinizer ignore-call */ \libphonenumber\PhoneNumberType::MOBILE; 371 | 372 | $phoneNumberObject = $phoneNumberUtil->parse($number, 'BR'); 373 | 374 | if ($phoneNumberUtil->isValidNumber($phoneNumberObject) === false || $phoneNumberUtil->getNumberType($phoneNumberObject) !== $mobilePhoneNumber) { 375 | throw new \Exception('Invalid phone number.'); 376 | } 377 | 378 | $number = $phoneNumberObject->getCountryCode().$phoneNumberObject->getNationalNumber(); 379 | } 380 | 381 | return (int) $number; 382 | } 383 | 384 | /** 385 | * Convert a date to format supported by the API. 386 | * 387 | * The API requires the date format d/m/Y, but in this class any valid date format is supported. 388 | * Since the API is always using the timezone America/Sao_Paulo, this function must also do timezone conversions. 389 | * 390 | * @see SmsDev::$dateFormat Date format to be used in all date functions. 391 | * 392 | * @param string $key The filter key to be set as a search filter. 393 | * @param string $date 394 | * 395 | * @return SmsDev 396 | */ 397 | private function parseDate($key, $date) 398 | { 399 | $parsedDate = \DateTime::createFromFormat($this->dateFormat, $date); 400 | 401 | if ($parsedDate !== false) { 402 | $parsedDate->setTimezone($this->apiTimeZone); 403 | 404 | $this->query[$key] = $parsedDate->format('d/m/Y'); 405 | } 406 | 407 | return $this; 408 | } 409 | 410 | /** 411 | * Sends a request to the smsdev.com.br API. 412 | * 413 | * @param \GuzzleHttp\Psr7\Request $request 414 | * 415 | * @return bool 416 | */ 417 | private function makeRequest($request) 418 | { 419 | $client = $this->getGuzzleClient(); 420 | 421 | try { 422 | $response = $client->send($request); 423 | } catch (\Exception $e) { 424 | return false; 425 | } 426 | 427 | $response = \json_decode($response->getBody(), true); 428 | 429 | if (\json_last_error() !== JSON_ERROR_NONE || \is_array($response) === false) { 430 | return false; 431 | } 432 | 433 | $this->_result = $response; 434 | 435 | return true; 436 | } 437 | 438 | /** 439 | * Creates GuzzleHttp\Client to be used in API requests. 440 | * This method is needed to test API calls in unit tests. 441 | * 442 | * @return object \GuzzleHttp\Client 443 | * 444 | * @codeCoverageIgnore 445 | */ 446 | protected function getGuzzleClient() 447 | { 448 | return new Client(); 449 | } 450 | } 451 | --------------------------------------------------------------------------------