Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 17 additions & 17 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,24 @@
*/
class Client extends ResourceCollection
{
const BASE_URL = 'https://rest.nexmo.com/';

/**
* @var string
*/
private $apiKey;

/**
* @var string
* @var array
*/
private $apiSecret;
private $options = array(
'apiKey' => null,
'apiSecret' => null,
'baseURL' => 'https://rest.nexmo.com/',
'debug' => false,
'timeout' => 5.0,
);

/**
* @param string $apiKey
* @param string $apiSecret
* @param array $options
*/
public function __construct($apiKey, $apiSecret)
public function __construct(Array $options = [])
{
$this->apiKey = $apiKey;
$this->apiSecret = $apiSecret;
// Override default options with options provided during instantiation.
$this->options = array_merge($this->options, $options);
}

protected function getNamespace()
Expand All @@ -57,11 +55,13 @@ protected function loadClient()
return;
}
$this->client = new HttpClient([
'base_url' => static::BASE_URL,
'base_url' => $this->options['baseURL'],
'defaults' => [
'timeout' => $this->options['timeout'],
'debug' => $this->options['debug'],
'query' => [
'api_key' => $this->apiKey,
'api_secret' => $this->apiSecret
'api_key' => $this->options['apiKey'],
'api_secret' => $this->options['apiSecret']
]
]
]);
Expand Down
81 changes: 81 additions & 0 deletions src/RateLimitSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php
namespace Nexmo;
use GuzzleHttp\Event\BeforeEvent;
use GuzzleHttp\Event\CompleteEvent;
use GuzzleHttp\Event\SubscriberInterface;

class RateLimitSubscriber implements SubscriberInterface
{
/**
* @param array $time
* Rate limit for the SMS and Voice API is on a per-LVN/shortcode basis, so we record durations in an array with the LVN as the key.
* For maximum throughput messages should be sent from different LVNs in an interleaved patter, e.g., 1, 2, 3, 1, 2, 3 instead of 1, 1, 2, 2, 3, 3
* (or better, send from each LVN in parallel, concurrent threads).
*/
private static $time = [];

/**
* @param float $max_requests_per_sec
* Max number of requests per second. Nexmo's most restrictive API is the SMS API for US/Canada -> US/Canada routing at 1/sec, let's be safe and use that as default.
*/
private $maxRequestsPerSec = 1;

/**
* @param string $timeKey
* LVN/shortcode when using the SMS or Voice API. Left as a blank string for the Developer API.
*/
private $timeKey = '';

/**
* @param int $rate
*/
public function setRate($rate)
{
$this->maxRequestsPerSec = $rate;
}

/**
* @param string $key
*/
public function setKey($key)
{
$this->timeKey = $key;
}

/**
* @return array
*/
public function getEvents()
{
return [
'before' => ['onBefore'],
'complete' => ['onComplete'],
];
}

/**
* @param BeforeEvent $event
*/
public function onBefore(BeforeEvent $event)
{
self::$time[$this->timeKey] = microtime(true);
}

/**
* @param CompleteEvent $event
*/
public function onComplete(CompleteEvent $event)
{
// Convert requests-per-second to a minimum duration in seconds.
$min_request_duration_sec = 1 / $this->maxRequestsPerSec;

// Calculate the duration of the previous request in seconds.
$elapsed_sec = microtime(true) - self::$time[$this->timeKey];

// If the duration is greater than the minimum duration, for this LVN, we must delay.
if ($elapsed_sec < $min_request_duration_sec) {
$min_request_duration_microsec = ($min_request_duration_sec - $elapsed_sec) * 1000000;
usleep($min_request_duration_microsec);
}
}
}
9 changes: 9 additions & 0 deletions src/Service/Account/Balance.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@
*/
class Balance extends Service
{
/**
* @inheritdoc
*/
public function getRateLimit()
{
// Max number of requests per second. Nexmo developer API claims 3/sec max, but actually more than 2/sec causes error 429 Too Many Requests.
return 2;
}

/**
* @inheritdoc
*/
Expand Down
16 changes: 13 additions & 3 deletions src/Service/Account/Numbers.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@
*/
class Numbers extends Service
{
/**
* @inheritdoc
*/
public function getRateLimit()
{
// Max number of requests per second. Nexmo developer API claims 3/sec max, but actually more than 2/sec causes error 429 Too Many Requests.
return 2;
}

/**
* @inheritdoc
*/
Expand Down Expand Up @@ -51,6 +60,10 @@ protected function validateResponse(array $json)
if (!isset($json['count'])) {
throw new Exception('count property expected');
}
if (0 == $json['count']) {
// If there are no numbers on the account, stop validating.
return;
}
if (!isset($json['numbers']) || !is_array($json['numbers'])) {
throw new Exception('numbers array property expected');
}
Expand All @@ -67,9 +80,6 @@ protected function validateResponse(array $json)
if (!isset($number['features']) || !is_array($number['features'])) {
throw new Exception('number.features array property expected');
}
if (!isset($number['moHttpUrl'])) {
throw new Exception('number.moHttpUrl property expected');
}
}
}
}
16 changes: 15 additions & 1 deletion src/Service/Account/Pricing/Country.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@
*/
class Country extends Service
{
/**
* @inheritdoc
*/
public function getRateLimit()
{
// Max number of requests per second. Nexmo developer API permits 3/sec max.
return 2;
}

/**
* @inheritdoc
*/
Expand All @@ -34,7 +43,8 @@ public function invoke($country = null)
}

return new Entity\Pricing($this->exec([
'country' => $country,
// Nexmo API requires $country value to be uppercase.
'country' => strtoupper($country),
]));
}

Expand All @@ -43,6 +53,10 @@ public function invoke($country = null)
*/
protected function validateResponse(array $json)
{
if (!isset($json['mt'])) {
// Some countries don't have any values, e.g., BV.
return;
}
if (!isset($json['country'])) {
throw new Exception('country property expected');
}
Expand Down
9 changes: 9 additions & 0 deletions src/Service/Account/Pricing/International.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@
*/
class International extends Country
{
/**
* @inheritdoc
*/
public function getRateLimit()
{
// Max number of requests per second. Nexmo developer API claims 3/sec max, but actually more than 2/sec causes error 429 Too Many Requests.
return 2;
}

/**
* @inheritdoc
*/
Expand Down
9 changes: 9 additions & 0 deletions src/Service/Account/Pricing/Phone.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@
*/
class Phone extends Service
{
/**
* @inheritdoc
*/
public function getRateLimit()
{
// Max number of requests per second. Nexmo developer API claims 3/sec max, but actually more than 2/sec causes error 429 Too Many Requests.
return 2;
}

/**
* @var string
*/
Expand Down
48 changes: 46 additions & 2 deletions src/Service/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@
*/
class Message extends Service
{
/**
* @inheritdoc
*/
public function getRateLimit($params = null)
{
if (preg_match('/^1\d{10}$/', $params['from']) && preg_match('/^1\d{10}$/', $params['to'])) {
// SMS from US/Canadian LVN to US/Canadian recipient has a 1/sec rate limit.
// https://help.nexmo.com/hc/en-us/articles/203993598
return 1;
}
// For all others, Nexmo Voice API has a 30/sec rate limit.
return 30;
}

/**
* @return string
*/
Expand Down Expand Up @@ -83,7 +97,8 @@ public function invoke(
throw new Exception("\$text parameter cannot be blank");
}

if ($type === 'text' && $this->containsUnicode($text)) {
// If $type is empty, 'text' will be assumed by Nexmo's SMS API.
if (($type == '' || $type === 'text') && $this->containsUnicode($text)) {
$type = 'unicode';
}

Expand All @@ -104,9 +119,38 @@ public function invoke(
]);
}

/**
* @param string $text
* @return int
*/
protected function containsUnicode($text)
{
return max(array_map('ord', str_split($text))) > 127;
// Valid GSM default character-set codepoint values from http://unicode.org/Public/MAPPINGS/ETSI/GSM0338.TXT
$gsm_0338_codepoints = [0x0040, 0x00A3, 0x0024, 0x00A5, 0x00E8, 0x00E9, 0x00F9, 0x00EC, 0x00F2, 0x00E7, 0x000A, 0x00D8, 0x00F8, 0x000D, 0x00C5, 0x00E5, 0x0394, 0x005F, 0x03A6, 0x0393, 0x039B, 0x03A9, 0x03A0, 0x03A8, 0x03A3, 0x0398, 0x039E, 0x00A0, 0x000C, 0x005E, 0x007B, 0x007D, 0x005C, 0x005B, 0x007E, 0x005D, 0x007C, 0x20AC, 0x00C6, 0x00E6, 0x00DF, 0x00C9, 0x0020, 0x0021, 0x0022, 0x0023, 0x00A4, 0x0025, 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F, 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F, 0x00A1, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058, 0x0059, 0x005A, 0x00C4, 0x00D6, 0x00D1, 0x00DC, 0x00A7, 0x00BF, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, 0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078, 0x0079, 0x007A, 0x00E4, 0x00F6, 0x00F1, 0x00FC, 0x00E0];

// Split $text into an array in a way that respects multibyte characters.
$text_chars = preg_split('//u', $text, null, PREG_SPLIT_NO_EMPTY);

// Array of codepoint values for characters in $text.
$text_codepoints = array_map([$this, 'uord'], $text_chars);

// Filter the array to contain only codepoints from $text that are not in the set of valid GSM codepoints.
$non_gsm_codepoints = array_diff($text_codepoints, $gsm_0338_codepoints);

// The text contains unicode if the result is not empty.
return !empty($non_gsm_codepoints);
}

/**
* @param char $unicode_char
* @return int
*/
public function uord($unicode_char)
{
$k = mb_convert_encoding($unicode_char, 'UCS-2LE', 'UTF-8');
$k1 = ord(substr($k, 0, 1));
$k2 = ord(substr($k, 1, 1));
return $k2 * 256 + $k1;
}

/**
Expand Down
40 changes: 40 additions & 0 deletions src/Service/Number.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace Nexmo\Service;

use Nexmo\Entity;
use Nexmo\Entity\MatchingStrategy;
use Nexmo\Exception;

/**
* Class Number
* @package Nexmo\Service
*/
class Number extends ResourceCollection
{
/**
* @return array
* @throws Exception
*/
public function search($country = null, $index = 1, $size = 10, $pattern = null, $searchPattern = MatchingStrategy::STARTS_WITH, $features = 'SMS')
{
return $this->search->invoke($country, $index, $size, $pattern, $searchPattern, $features);
}

/**
* @return array
* @throws Exception
*/
public function buy($country, $msisdn)
{
return $this->buy->invoke($country, $msisdn);
}
/**
* @return array
* @throws Exception
*/
public function cancel($country, $msisdn)
{
return $this->cancel->invoke($country, $msisdn);
}
}
Loading