├── .gitignore ├── LICENSE ├── README ├── composer.json ├── example.php └── src ├── NexmoAccount.php ├── NexmoMessage.php └── NexmoReceipt.php /.gitignore: -------------------------------------------------------------------------------- 1 | .DS* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Darren Whitlen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Sending SMS via the Nexmo SMS gateway. 2 | 3 | 4 | Quick Examples 5 | -------------- 6 | 7 | 1) Sending an SMS 8 | 9 | $sms = new NexmoMessage('account_key', 'account_secret'); 10 | $sms->sendText( '+447234567890', 'MyApp', 'Hello world!' ); 11 | 12 | 13 | 14 | 2) Recieving SMS 15 | 16 | $sms = new NexmoMessage('account_key', 'account_secret'); 17 | if ($sms->inboundText()) { 18 | $sms->reply('You said: ' . $sms->text); 19 | } 20 | 21 | 22 | 23 | 3) Recieving a message receipt 24 | 25 | $receipt = new NexmoReceipt(); 26 | if ($receipt->exists()) { 27 | switch ($receipt->status) { 28 | case $receipt::STATUS_DELIVERED: 29 | // The message was delivered to the handset! 30 | break; 31 | 32 | case $receipt::STATUS_FAILED: 33 | case $receipt::STATUS_EXPIRED: 34 | // The message failed to be delivered 35 | break; 36 | } 37 | } 38 | 39 | 40 | 41 | 4) List purchased numbers on your account 42 | 43 | $account = new NexmoAccount('account_key', 'account_secret'); 44 | $numbers = $account->numbersList(); 45 | 46 | 47 | 48 | 49 | 50 | 51 | Most Frequent Issues 52 | -------------------- 53 | 54 | Sending a message returns false. 55 | 56 | This is usually due to your webserver unable to send a request to 57 | Nexmo. Make sure the following are met: 58 | 59 | 1) Either CURL is enabled for your PHP installation or the PHP 60 | option 'allow_url_fopen' is set to 1 (default). 61 | 62 | 2) You have no firewalls blocking access to rest.nexmo.com/sms/json 63 | on port 443. 64 | 65 | 66 | 67 | Your message appears to have been sent but you do not recieve it. 68 | 69 | Run the example.php file included. This will show any errors that 70 | are returned from Nexmo. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prawnsalad/nexmo", 3 | "description": "prawnsalad's Nexmo-PHP-lib", 4 | "keywords": ["Nexmo"], 5 | "homepage": "https://github.com/prawnsalad/Nexmo-PHP-lib", 6 | "license": "MIT", 7 | "autoload": { 8 | "psr-4": { 9 | "PrawnSalad\\Nexmo\\": ["src"] 10 | } 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /example.php: -------------------------------------------------------------------------------- 1 | sendText( '+447234567890', 'MyApp', 'Hello!' ); 18 | 19 | // Step 3: Display an overview of the message 20 | echo $nexmo_sms->displayOverview($info); 21 | 22 | // Done! 23 | 24 | ?> -------------------------------------------------------------------------------- /src/NexmoAccount.php: -------------------------------------------------------------------------------- 1 | array('method' => 'GET', 'url' => '/account/get-balance/{k}/{s}'), 25 | 'get_pricing' => array('method' => 'GET', 'url' => '/account/get-pricing/outbound/{k}/{s}/{country_code}'), 26 | 'get_own_numbers' => array('method' => 'GET', 'url' => '/account/numbers/{k}/{s}'), 27 | 'search_numbers' => array('method' => 'GET', 'url' => '/number/search/{k}/{s}/{country_code}?pattern={pattern}'), 28 | 'buy_number' => array('method' => 'POST', 'url' => '/number/buy/{k}/{s}/{country_code}/{msisdn}'), 29 | 'cancel_number' => array('method' => 'POST', 'url' => '/number/cancel/{k}/{s}/{country_code}/{msisdn}') 30 | ); 31 | 32 | 33 | private $cache = array(); 34 | 35 | /** 36 | * @param $nx_key Your Nexmo account key 37 | * @param $nx_secret Your Nexmo secret 38 | */ 39 | public function __construct ($api_key, $api_secret) { 40 | $this->nx_key = $api_key; 41 | $this->nx_secret = $api_secret; 42 | } 43 | 44 | 45 | /** 46 | * Return your account balance in Euros 47 | * @return float|bool 48 | */ 49 | public function balance () { 50 | if (!isset($this->cache['balance'])) { 51 | $tmp = $this->apiCall('get_balance'); 52 | if (!$tmp['data']) return false; 53 | 54 | $this->cache['balance'] = $tmp['data']['value']; 55 | } 56 | 57 | return (float)$this->cache['balance']; 58 | } 59 | 60 | 61 | /** 62 | * Find out the price to send a message to a country 63 | * @param $country_code Country code to return the SMS price for 64 | * @return float|bool 65 | */ 66 | public function smsPricing ($country_code) { 67 | $country_code = strtoupper($country_code); 68 | 69 | if (!isset($this->cache['country_codes'])) 70 | $this->cache['country_codes'] = array(); 71 | 72 | if (!isset($this->cache['country_codes'][$country_code])) { 73 | $tmp = $this->apiCall('get_pricing', array('country_code'=>$country_code)); 74 | if (!$tmp['data']) return false; 75 | 76 | $this->cache['country_codes'][$country_code] = $tmp['data']; 77 | } 78 | 79 | return (float)$this->cache['country_codes'][$country_code]['mt']; 80 | } 81 | 82 | 83 | /** 84 | * Return a countries international dialing code 85 | * @param $country_code Country code to return the dialing code for 86 | * @return string|bool 87 | */ 88 | public function getCountryDialingCode ($country_code) { 89 | $country_code = strtoupper($country_code); 90 | 91 | if (!isset($this->cache['country_codes'])) 92 | $this->cache['country_codes'] = array(); 93 | 94 | if (!isset($this->cache['country_codes'][$country_code])) { 95 | $tmp = $this->apiCall('get_pricing', array('country_code'=>$country_code)); 96 | if (!$tmp['data']) return false; 97 | 98 | $this->cache['country_codes'][$country_code] = $tmp['data']; 99 | } 100 | 101 | return (string)$this->cache['country_codes'][$country_code]['prefix']; 102 | } 103 | 104 | 105 | /** 106 | * Get an array of all purchased numbers for your account 107 | * @return array|bool 108 | */ 109 | public function numbersList () { 110 | if (!isset($this->cache['own_numbers'])) { 111 | $tmp = $this->apiCall('get_own_numbers'); 112 | if (!$tmp['data']) return false; 113 | 114 | $this->cache['own_numbers'] = $tmp['data']; 115 | } 116 | 117 | if (!$this->cache['own_numbers']['numbers']) { 118 | return array(); 119 | } 120 | 121 | return $this->cache['own_numbers']['numbers']; 122 | } 123 | 124 | 125 | /** 126 | * Search available numbers to purchase for your account 127 | * @param $country_code Country code to search available numbers in 128 | * @param $pattern Number pattern to search for 129 | * @return bool 130 | */ 131 | public function numbersSearch ($country_code, $pattern) { 132 | $country_code = strtoupper($country_code); 133 | 134 | $tmp = $this->apiCall('search_numbers', array('country_code'=>$country_code, 'pattern'=>$pattern)); 135 | if (!$tmp['data'] || !isset($tmp['data']['numbers'])) return false; 136 | return $tmp['data']['numbers']; 137 | } 138 | 139 | 140 | /** 141 | * Purchase an available number to your account 142 | * @param $country_code Country code for your desired number 143 | * @param $msisdn Full number which you wish to purchase 144 | * @return bool 145 | */ 146 | public function numbersBuy ($country_code, $msisdn) { 147 | $country_code = strtoupper($country_code); 148 | 149 | $tmp = $this->apiCall('buy_number', array('country_code'=>$country_code, 'msisdn'=>$msisdn)); 150 | return ($tmp['http_code'] === 200); 151 | } 152 | 153 | 154 | /** 155 | * Cancel an existing number on your account 156 | * @param $country_code Country code for which the number is for 157 | * @param $msisdn The number to cancel 158 | * @return bool 159 | */ 160 | public function numbersCancel ($country_code, $msisdn) { 161 | $country_code = strtoupper($country_code); 162 | 163 | $tmp = $this->apiCall('cancel_number', array('country_code'=>$country_code, 'msisdn'=>$msisdn)); 164 | return ($tmp['http_code'] === 200); 165 | } 166 | 167 | 168 | /** 169 | * Run a REST command on Nexmo SMS services 170 | * @param $command 171 | * @param array $data 172 | * @return array|bool 173 | */ 174 | private function apiCall($command, $data=array()) { 175 | if (!isset($this->rest_commands[$command])) { 176 | return false; 177 | } 178 | 179 | $cmd = $this->rest_commands[$command]; 180 | 181 | $url = $cmd['url']; 182 | $url = str_replace(array('{k}', '{s}') ,array($this->nx_key, $this->nx_secret), $url); 183 | 184 | $parsed_data = array(); 185 | foreach ($data as $k => $v) $parsed_data['{'.$k.'}'] = $v; 186 | $url = str_replace(array_keys($parsed_data) ,array_values($parsed_data), $url); 187 | 188 | $url = trim($this->rest_base_url, '/') . $url; 189 | $post_data = ''; 190 | 191 | // If available, use CURL 192 | if (function_exists('curl_version')) { 193 | 194 | $to_nexmo = curl_init( $url ); 195 | curl_setopt( $to_nexmo, CURLOPT_RETURNTRANSFER, true ); 196 | curl_setopt( $to_nexmo, CURLOPT_SSL_VERIFYPEER, false); 197 | curl_setopt($to_nexmo, CURLOPT_HTTPHEADER, array('Accept: application/json')); 198 | 199 | if ($cmd['method'] == 'POST') { 200 | curl_setopt( $to_nexmo, CURLOPT_POST, true ); 201 | curl_setopt( $to_nexmo, CURLOPT_POSTFIELDS, $post_data ); 202 | } 203 | 204 | $from_nexmo = curl_exec( $to_nexmo ); 205 | $curl_info = curl_getinfo($to_nexmo); 206 | $http_response_code = $curl_info['http_code']; 207 | curl_close ( $to_nexmo ); 208 | 209 | } elseif (ini_get('allow_url_fopen')) { 210 | // No CURL available so try the awesome file_get_contents 211 | 212 | $opts = array('http' => 213 | array( 214 | 'method' => 'GET', 215 | 'header' => 'Accept: application/json' 216 | ) 217 | ); 218 | 219 | if ($cmd['method'] == 'POST') { 220 | $opts['http']['method'] = 'POST'; 221 | $opts['http']['header'] .= "\r\nContent-type: application/x-www-form-urlencoded"; 222 | $opts['http']['content'] = $post_data; 223 | } 224 | 225 | $context = stream_context_create($opts); 226 | $from_nexmo = file_get_contents($url, false, $context); 227 | 228 | // et the response code 229 | preg_match('/HTTP\/[^ ]+ ([0-9]+)/i', $http_response_header[0], $m); 230 | $http_response_code = $m[1]; 231 | 232 | } else { 233 | // No way of sending a HTTP post :( 234 | return false; 235 | } 236 | 237 | $data = json_decode($from_nexmo, true); 238 | return array( 239 | 'data' => $data, 240 | 'http_code' => (int)$http_response_code 241 | ); 242 | 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/NexmoMessage.php: -------------------------------------------------------------------------------- 1 | nx_key = $api_key; 61 | $this->nx_secret = $api_secret; 62 | } 63 | 64 | 65 | 66 | /** 67 | * Prepare new text message. 68 | * 69 | * If $unicode is not provided we will try to detect the 70 | * message type. Otherwise set to TRUE if you require 71 | * unicode characters. 72 | */ 73 | function sendText ( $to, $from, $message, $unicode=null ) { 74 | 75 | // Making sure strings are UTF-8 encoded 76 | if ( !is_numeric($from) && !mb_check_encoding($from, 'UTF-8') ) { 77 | trigger_error('$from needs to be a valid UTF-8 encoded string'); 78 | return false; 79 | } 80 | 81 | if ( !mb_check_encoding($message, 'UTF-8') ) { 82 | trigger_error('$message needs to be a valid UTF-8 encoded string'); 83 | return false; 84 | } 85 | 86 | if ($unicode === null) { 87 | $containsUnicode = max(array_map('ord', str_split($message))) > 127; 88 | } else { 89 | $containsUnicode = (bool)$unicode; 90 | } 91 | 92 | // Make sure $from is valid 93 | $from = $this->validateOriginator($from); 94 | 95 | // URL Encode 96 | $from = urlencode( $from ); 97 | $message = urlencode( $message ); 98 | 99 | // Send away! 100 | $post = array( 101 | 'from' => $from, 102 | 'to' => $to, 103 | 'text' => $message, 104 | 'type' => $containsUnicode ? 'unicode' : 'text' 105 | ); 106 | return $this->sendRequest ( $post ); 107 | 108 | } 109 | 110 | 111 | /** 112 | * Prepare new WAP message. 113 | */ 114 | function sendBinary ( $to, $from, $body, $udh ) { 115 | 116 | //Binary messages must be hex encoded 117 | $body = bin2hex ( $body ); 118 | $udh = bin2hex ( $udh ); 119 | 120 | // Make sure $from is valid 121 | $from = $this->validateOriginator($from); 122 | 123 | // Send away! 124 | $post = array( 125 | 'from' => $from, 126 | 'to' => $to, 127 | 'type' => 'binary', 128 | 'body' => $body, 129 | 'udh' => $udh 130 | ); 131 | return $this->sendRequest ( $post ); 132 | 133 | } 134 | 135 | 136 | /** 137 | * Prepare new binary message. 138 | */ 139 | function pushWap ( $to, $from, $title, $url, $validity = 172800000 ) { 140 | 141 | // Making sure $title and $url are UTF-8 encoded 142 | if ( !mb_check_encoding($title, 'UTF-8') || !mb_check_encoding($url, 'UTF-8') ) { 143 | trigger_error('$title and $udh need to be valid UTF-8 encoded strings'); 144 | return false; 145 | } 146 | 147 | // Make sure $from is valid 148 | $from = $this->validateOriginator($from); 149 | 150 | // Send away! 151 | $post = array( 152 | 'from' => $from, 153 | 'to' => $to, 154 | 'type' => 'wappush', 155 | 'url' => $url, 156 | 'title' => $title, 157 | 'validity' => $validity 158 | ); 159 | return $this->sendRequest ( $post ); 160 | 161 | } 162 | 163 | 164 | /** 165 | * Prepare and send a new message. 166 | */ 167 | private function sendRequest ( $data ) { 168 | // Build the post data 169 | $data = array_merge($data, array('username' => $this->nx_key, 'password' => $this->nx_secret)); 170 | $post = ''; 171 | foreach($data as $k => $v){ 172 | $post .= "&$k=$v"; 173 | } 174 | 175 | // If available, use CURL 176 | if (function_exists('curl_version')) { 177 | 178 | $to_nexmo = curl_init( $this->nx_uri ); 179 | curl_setopt( $to_nexmo, CURLOPT_POST, true ); 180 | curl_setopt( $to_nexmo, CURLOPT_RETURNTRANSFER, true ); 181 | curl_setopt( $to_nexmo, CURLOPT_POSTFIELDS, $post ); 182 | 183 | if (!$this->ssl_verify) { 184 | curl_setopt( $to_nexmo, CURLOPT_SSL_VERIFYPEER, false); 185 | } 186 | 187 | $from_nexmo = curl_exec( $to_nexmo ); 188 | curl_close ( $to_nexmo ); 189 | 190 | } elseif (ini_get('allow_url_fopen')) { 191 | // No CURL available so try the awesome file_get_contents 192 | 193 | $opts = array('http' => 194 | array( 195 | 'method' => 'POST', 196 | 'header' => 'Content-type: application/x-www-form-urlencoded', 197 | 'content' => $post 198 | ) 199 | ); 200 | $context = stream_context_create($opts); 201 | $from_nexmo = file_get_contents($this->nx_uri, false, $context); 202 | 203 | } else { 204 | // No way of sending a HTTP post :( 205 | return false; 206 | } 207 | 208 | 209 | return $this->nexmoParse( $from_nexmo ); 210 | 211 | } 212 | 213 | 214 | /** 215 | * Recursively normalise any key names in an object, removing unwanted characters 216 | */ 217 | private function normaliseKeys ($obj) { 218 | // Determine is working with a class or araay 219 | if ($obj instanceof stdClass) { 220 | $new_obj = new stdClass(); 221 | $is_obj = true; 222 | } else { 223 | $new_obj = array(); 224 | $is_obj = false; 225 | } 226 | 227 | 228 | foreach($obj as $key => $val){ 229 | // If we come across another class/array, normalise it 230 | if ($val instanceof stdClass || is_array($val)) { 231 | $val = $this->normaliseKeys($val); 232 | } 233 | 234 | // Replace any unwanted characters in they key name 235 | if ($is_obj) { 236 | $new_obj->{str_replace('-', '', $key)} = $val; 237 | } else { 238 | $new_obj[str_replace('-', '', $key)] = $val; 239 | } 240 | } 241 | 242 | return $new_obj; 243 | } 244 | 245 | 246 | /** 247 | * Parse server response. 248 | */ 249 | private function nexmoParse ( $from_nexmo ) { 250 | $response = json_decode($from_nexmo); 251 | 252 | // Copy the response data into an object, removing any '-' characters from the key 253 | $response_obj = $this->normaliseKeys($response); 254 | 255 | if ($response_obj) { 256 | $this->nexmo_response = $response_obj; 257 | 258 | // Find the total cost of this message 259 | $response_obj->cost = $total_cost = 0; 260 | if (is_array($response_obj->messages)) { 261 | foreach ($response_obj->messages as $msg) { 262 | if (property_exists($msg, "messageprice")) { 263 | $total_cost = $total_cost + (float)$msg->messageprice; 264 | } 265 | } 266 | 267 | $response_obj->cost = $total_cost; 268 | } 269 | 270 | return $response_obj; 271 | 272 | } else { 273 | // A malformed response 274 | $this->nexmo_response = array(); 275 | return false; 276 | } 277 | 278 | } 279 | 280 | 281 | /** 282 | * Validate an originator string 283 | * 284 | * If the originator ('from' field) is invalid, some networks may reject the network 285 | * whilst stinging you with the financial cost! While this cannot correct them, it 286 | * will try its best to correctly format them. 287 | */ 288 | private function validateOriginator($inp){ 289 | // Remove any invalid characters 290 | $ret = preg_replace('/[^a-zA-Z0-9]/', '', (string)$inp); 291 | 292 | if(preg_match('/[a-zA-Z]/', $inp)){ 293 | 294 | // Alphanumeric format so make sure it's < 11 chars 295 | $ret = substr($ret, 0, 11); 296 | 297 | } else { 298 | 299 | // Numerical, remove any prepending '00' 300 | if(substr($ret, 0, 2) == '00'){ 301 | $ret = substr($ret, 2); 302 | $ret = substr($ret, 0, 15); 303 | } 304 | } 305 | 306 | return (string)$ret; 307 | } 308 | 309 | 310 | 311 | /** 312 | * Display a brief overview of a sent message. 313 | * Useful for debugging and quick-start purposes. 314 | */ 315 | public function displayOverview( $nexmo_response=null ){ 316 | $info = (!$nexmo_response) ? $this->nexmo_response : $nexmo_response; 317 | 318 | if (!$nexmo_response ) return 'Cannot display an overview of this response'; 319 | 320 | // How many messages were sent? 321 | if ( $info->messagecount > 1 ) { 322 | 323 | $status = 'Your message was sent in ' . $info->messagecount . ' parts'; 324 | 325 | } elseif ( $info->messagecount == 1) { 326 | 327 | $status = 'Your message was sent'; 328 | 329 | } else { 330 | 331 | return 'There was an error sending your message'; 332 | } 333 | 334 | // Build an array of each message status and ID 335 | if (!is_array($info->messages)) $info->messages = array(); 336 | $message_status = array(); 337 | foreach ( $info->messages as $message ) { 338 | $tmp = array('id'=>'', 'status'=>0); 339 | 340 | if ( $message->status != 0) { 341 | $tmp['status'] = $message->errortext; 342 | } else { 343 | $tmp['status'] = 'OK'; 344 | $tmp['id'] = $message->messageid; 345 | } 346 | 347 | $message_status[] = $tmp; 348 | } 349 | 350 | 351 | // Build the output 352 | if (isset($_SERVER['HTTP_HOST'])) { 353 | // HTML output 354 | $ret = '
'.$status.' | |
Status | Message ID |
---|---|
'.$mstat['status'].' | '.$mstat['id'].' |