├── VERSION ├── CHANGELOG.md ├── .coveralls.yml ├── lib ├── Error │ ├── MomoApiError.php │ ├── ApiConnection.php │ ├── Authentication.php │ ├── InvalidRequest.php │ └── Base.php ├── test.php ├── Util │ ├── DefaultLogger.php │ ├── RandomGenerator.php │ ├── LoggerInterface.php │ ├── CaseInsensitiveArray.php │ ├── RequestOptions.php │ └── Util.php ├── models │ ├── LoginBody.php │ ├── Payer.php │ ├── Account.php │ ├── Balance.php │ ├── AccessToken.php │ ├── Transfer.php │ ├── Transaction.php │ ├── RequestToPay.php │ └── ResourceFactory.php ├── ApiResponse.php ├── HttpClient │ ├── ClientInterface.php │ └── CurlClient.php ├── Provision.php ├── Collection.php ├── Remittance.php ├── Disbursement.php ├── ApiRequest.php └── MomoApi.php ├── phpunit.no_autoload.xml ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── composer.json ├── LICENSE.md ├── init.php └── README.md /VERSION: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | coverage_clover: clover.xml 3 | json_path: coveralls-upload.json -------------------------------------------------------------------------------- /lib/Error/MomoApiError.php: -------------------------------------------------------------------------------- 1 | getToken(); 20 | } 21 | -------------------------------------------------------------------------------- /phpunit.no_autoload.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tests 5 | 6 | 7 | 8 | 9 | lib 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /lib/Util/DefaultLogger.php: -------------------------------------------------------------------------------- 1 | 0) { 14 | throw new \Exception('DefaultLogger does not currently implement context. Please implement if you need it.'); 15 | } 16 | error_log($message); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/models/LoginBody.php: -------------------------------------------------------------------------------- 1 | user_id = $user_id; 16 | $this->api_key = $api_key; 17 | } 18 | 19 | 20 | public function jsonSerialize() 21 | { 22 | $data = array( 23 | 'user_id' => $this->user_id, 24 | 'api_key' => $this->api_key, 25 | 26 | ); 27 | 28 | return $data; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/models/Payer.php: -------------------------------------------------------------------------------- 1 | partyIdType = $partyIdType; 16 | $this->partyId = $partyId; 17 | } 18 | 19 | 20 | public function jsonSerialize() 21 | { 22 | $data = array( 23 | 'partyIdType' => $this->partyIdType, 24 | 'partyId' => $this->partyId, 25 | 26 | ); 27 | 28 | return $data; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/models/Account.php: -------------------------------------------------------------------------------- 1 | availableBalance = $availableBalance; 17 | $this->currency = $currency; 18 | } 19 | 20 | 21 | public function jsonSerialize() 22 | { 23 | $data = array( 24 | 'availableBalance' => $this->availableBalance, 25 | 'currency' => $this->currency 26 | ); 27 | 28 | return $data; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /lib/ApiResponse.php: -------------------------------------------------------------------------------- 1 | body = $body; 28 | $this->code = $code; 29 | $this->headers = $headers; 30 | $this->json = $json; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/models/Balance.php: -------------------------------------------------------------------------------- 1 | description = $description; 16 | $this->availableBalance = $availableBalance; 17 | $this->currency = $currency; 18 | } 19 | 20 | public function jsonSerialize() 21 | { 22 | $data = array( 23 | 'description' => $this->description, 24 | 'availableBalance' => $this->availableBalance, 25 | 'currency' => $this->currency 26 | ); 27 | 28 | return $data; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/models/AccessToken.php: -------------------------------------------------------------------------------- 1 | access_token = $access_token; 18 | $this->token_type = $token_type; 19 | $this->expires_in = $expires_in; 20 | } 21 | 22 | 23 | public function jsonSerialize() 24 | { 25 | $data = array( 26 | 'access_token' => $this->access_token, 27 | 'token_type' => $this->token_type, 28 | 'expires_in' => $this->expires_in 29 | ); 30 | 31 | return $data; 32 | } 33 | 34 | public function getToken() 35 | { 36 | return $this->access_token; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sparkplug\/momoapi-php", 3 | "description": "MTN momo bindings to PHP", 4 | "type": "library", 5 | "keywords": [ 6 | "mtn momo", 7 | "payment processing", 8 | "api" 9 | ], 10 | "require": { 11 | "php": ">=5.4.0", 12 | "ext-curl": "*", 13 | "ext-json": "*", 14 | "ext-mbstring": "*", 15 | "adhocore\/cli": "^0.3.3" 16 | }, 17 | "require-dev": { 18 | "phpunit\/phpunit": "~4.0", 19 | "php-coveralls\/php-coveralls": "1.1.*", 20 | "squizlabs/php_codesniffer": "~2.0", 21 | "symfony\/process": "~2.8" 22 | }, 23 | "license": "MIT", 24 | "authors": [ 25 | { 26 | "name": "Moses Mugisha", 27 | "email": "mossplix@gmail.com" 28 | } 29 | ], 30 | "minimum-stability": "stable", 31 | 32 | "autoload": { 33 | "psr-4": { "MomoApi\\" : "lib/" } 34 | } 35 | } -------------------------------------------------------------------------------- /lib/HttpClient/ClientInterface.php: -------------------------------------------------------------------------------- 1 | 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 13 | > all 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 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/models/Transfer.php: -------------------------------------------------------------------------------- 1 | payee = $payee; 24 | $this->payeeNote = $payeeNote; 25 | $this->currency = $currency; 26 | 27 | $this->payerMessage = $payerMessage; 28 | $this->externalId = $externalId; 29 | $this->amount = $amount; 30 | } 31 | 32 | 33 | public function jsonSerialize() 34 | { 35 | $data = array( 36 | 'payee' => array($this->payer->partyIdType, $this->payer->partyId), 37 | 'payeeNote' => $this->payeeNote, 38 | 'currency' => $this->currency, 39 | 40 | 'payerMessage' => $this->payerMessage, 41 | 'externalId' => $this->externalId, 42 | 'amount' => $this->amount, 43 | ); 44 | 45 | return $data; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/models/Transaction.php: -------------------------------------------------------------------------------- 1 | amount = $amount; 22 | $this->currency = $currency; 23 | $this->financialTransactionId = $financialTransactionId; 24 | 25 | $this->externalId = $externalId; 26 | $this->payer = $payer; 27 | $this->status = $status; 28 | $this->reason = $reason; 29 | } 30 | 31 | 32 | public function jsonSerialize() 33 | { 34 | $data = array( 35 | 'amount' => $this->amount, 36 | 'currency' => $this->currency, 37 | 'financialTransactionId' => $this->financialTransactionId, 38 | 'externalId' => $this->externalId, 39 | 'payer' => $this->payer, 40 | 'status' => $this->status, 41 | 'reason' => $this->reason 42 | 43 | 44 | ); 45 | 46 | return $data; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/Util/LoggerInterface.php: -------------------------------------------------------------------------------- 1 | payer = $payer; 25 | $this->payeeNote = $payeeNote; 26 | $this->payerMessage = $payerMessage; 27 | $this->externalId = $externalId; 28 | $this->currency = $currency; 29 | $this->amount = $amount; 30 | $this->status = $status; 31 | $this->financialTransactionId = $financialTransactionId; 32 | } 33 | 34 | 35 | public function jsonSerialize() 36 | { 37 | $data = array( 38 | 'payer' => array($this->payer->partyIdType, $this->payer->partyId), 39 | 'payeeNote' => $this->payeeNote, 40 | 'payerMessage' => $this->payerMessage, 41 | 'externalId' => $this->externalId, 42 | 'currency' => $this->currency, 43 | 'amount' => $this->amount, 44 | 'status' => $this->status, 45 | 'financialTransactionId' => $this->financialTransactionId 46 | 47 | ); 48 | 49 | return $data; 50 | } 51 | 52 | public function getStatus() 53 | { 54 | return $this->status; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/Error/Base.php: -------------------------------------------------------------------------------- 1 | httpStatus = $httpStatus; 18 | $this->httpBody = $httpBody; 19 | $this->jsonBody = $jsonBody; 20 | $this->httpHeaders = $httpHeaders; 21 | $this->requestId = null; 22 | 23 | // TODO: make this a proper constructor argument in the next major 24 | // release. 25 | $this->stripeCode = isset($jsonBody["error"]["code"]) ? $jsonBody["error"]["code"] : null; 26 | 27 | if ($httpHeaders && isset($httpHeaders['Request-Id'])) { 28 | $this->requestId = $httpHeaders['Request-Id']; 29 | } 30 | } 31 | 32 | public function getMomoApiCode() 33 | { 34 | return $this->stripeCode; 35 | } 36 | 37 | public function getHttpStatus() 38 | { 39 | return $this->httpStatus; 40 | } 41 | 42 | public function getHttpBody() 43 | { 44 | return $this->httpBody; 45 | } 46 | 47 | public function getJsonBody() 48 | { 49 | return $this->jsonBody; 50 | } 51 | 52 | public function getHttpHeaders() 53 | { 54 | return $this->httpHeaders; 55 | } 56 | 57 | public function getRequestId() 58 | { 59 | return $this->requestId; 60 | } 61 | 62 | public function __toString() 63 | { 64 | $id = $this->requestId ? " from API request '{$this->requestId}'" : ""; 65 | $message = explode("\n", parent::__toString()); 66 | $message[0] .= $id; 67 | return implode("\n", $message); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /init.php: -------------------------------------------------------------------------------- 1 | container = array_map("strtolower", $initial_array); 24 | } 25 | 26 | public function offsetSet($offset, $value) 27 | { 28 | $offset = static::maybeLowercase($offset); 29 | if (is_null($offset)) { 30 | $this->container[] = $value; 31 | } else { 32 | $this->container[$offset] = $value; 33 | } 34 | } 35 | 36 | public function offsetExists($offset) 37 | { 38 | $offset = static::maybeLowercase($offset); 39 | return isset($this->container[$offset]); 40 | } 41 | 42 | public function offsetUnset($offset) 43 | { 44 | $offset = static::maybeLowercase($offset); 45 | unset($this->container[$offset]); 46 | } 47 | 48 | public function offsetGet($offset) 49 | { 50 | $offset = static::maybeLowercase($offset); 51 | return isset($this->container[$offset]) ? $this->container[$offset] : null; 52 | } 53 | 54 | private static function maybeLowercase($v) 55 | { 56 | if (is_string($v)) { 57 | return strtolower($v); 58 | } else { 59 | return $v; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/models/ResourceFactory.php: -------------------------------------------------------------------------------- 1 | trim($host))); 18 | 19 | $url = 'https://ericssonbasicapi2.azure-api.net/v1_0/apiuser'; 20 | 21 | $token = Util\Util::uuid(); 22 | $ch = curl_init(); 23 | 24 | $userUrl = "https://ericssonbasicapi2.azure-api.net/v1_0/apiuser/" . $token . "/apikey"; 25 | 26 | //curl_setopt($ch, CURLOPT_POST, 1); 27 | //curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "post"); 28 | 29 | curl_setopt($ch, CURLOPT_POST, true); 30 | curl_setopt($ch, CURLOPT_POSTFIELDS, $data); 31 | curl_setopt($ch, CURLOPT_FAILONERROR, true); 32 | //curl_setopt($ch, CURLOPT_VERBOSE, 1); 33 | curl_setopt($ch, CURLOPT_URL, $url); 34 | //curl_setopt($ch, CURLOPT_HEADER,false); 35 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); 36 | curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); 37 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 38 | //curl_setopt($ch, CURLINFO_HEADER_OUT, true); 39 | curl_setopt($ch, CURLOPT_POSTFIELDS, $data); 40 | curl_setopt( 41 | $ch, 42 | CURLOPT_HTTPHEADER, 43 | array('Content-Type: application/json', 44 | 'X-Reference-Id: ' . $token, 45 | 'Accept: application/json', 46 | 'Ocp-Apim-Subscription-Key: ' . trim($apiKey) 47 | ) 48 | ); 49 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 50 | $result = curl_exec($ch); 51 | 52 | if ($result) { 53 | curl_setopt($ch, CURLOPT_URL, $userUrl); 54 | 55 | curl_setopt( 56 | $ch, 57 | CURLOPT_HTTPHEADER, 58 | array('Content-Type: application/json', 59 | 'Accept: application/json', 60 | 'Ocp-Apim-Subscription-Key: ' . trim($apiKey) 61 | ) 62 | ); 63 | 64 | 65 | $result2 = curl_exec($ch); 66 | 67 | 68 | curl_close($ch); 69 | echo $result; 70 | echo $result2; 71 | $res = json_decode($result2, true); 72 | 73 | 74 | echo "Here is your User Id and API secret : {UserId:" . $token . " , APISecret: " . $res["apiKey"] . " }"; 75 | } 76 | else{ 77 | echo "something went wrong"; 78 | } 79 | } 80 | } 81 | if (!debug_backtrace()) { 82 | $obj = new Provision(); 83 | $obj->getCredentials(); 84 | } 85 | -------------------------------------------------------------------------------- /lib/Util/RequestOptions.php: -------------------------------------------------------------------------------- 1 | apiKey = $key; 24 | $this->headers = $headers; 25 | $this->apiBase = $base; 26 | } 27 | 28 | /** 29 | * Unpacks an options array and merges it into the existing RequestOptions 30 | * object. 31 | * 32 | * @param array|string|null $options a key => value array 33 | * 34 | * @return RequestOptions 35 | */ 36 | public function merge($options) 37 | { 38 | $other_options = self::parse($options); 39 | if ($other_options->apiKey === null) { 40 | $other_options->apiKey = $this->apiKey; 41 | } 42 | if ($other_options->apiBase === null) { 43 | $other_options->apiBase = $this->apiBase; 44 | } 45 | $other_options->headers = array_merge($this->headers, $other_options->headers); 46 | return $other_options; 47 | } 48 | 49 | /** 50 | * Discards all headers that we don't want to persist across requests. 51 | */ 52 | public function discardNonPersistentHeaders() 53 | { 54 | foreach ($this->headers as $k => $v) { 55 | if (!in_array($k, self::$HEADERS_TO_PERSIST)) { 56 | unset($this->headers[$k]); 57 | } 58 | } 59 | } 60 | 61 | /** 62 | * Unpacks an options array into an RequestOptions object 63 | * 64 | * @param array|string|null $options a key => value array 65 | * 66 | * @return RequestOptions 67 | */ 68 | public static function parse($options) 69 | { 70 | if ($options instanceof self) { 71 | return $options; 72 | } 73 | 74 | if (is_null($options)) { 75 | return new RequestOptions(null, [], null); 76 | } 77 | 78 | if (is_string($options)) { 79 | return new RequestOptions($options, [], null); 80 | } 81 | 82 | if (is_array($options)) { 83 | $headers = []; 84 | $key = null; 85 | $base = null; 86 | if (array_key_exists('api_key', $options)) { 87 | $key = $options['api_key']; 88 | } 89 | 90 | return new RequestOptions($key, $headers, $base); 91 | } 92 | 93 | $message = 'The second argument to MomoApi API method calls is an ' 94 | . 'optional per-request apiKey, which must be a string, or ' 95 | . 'per-request options, which must be an array. (HINT: you can set ' 96 | . 'a global apiKey by "MomoApi::setApiKey()")'; 97 | throw new Error\Api($message); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/Collection.php: -------------------------------------------------------------------------------- 1 | _currency = $currency; 51 | 52 | 53 | if (!$baseUrl) { 54 | $baseUrl = MomoApi::getBaseUrl(); 55 | } 56 | $this->_baseUrl = $baseUrl; 57 | 58 | 59 | if (!$targetEnvironment) { 60 | $targetEnvironment = MomoApi::getTargetEnvironment(); 61 | } 62 | $this->_targetEnvironment = $targetEnvironment; 63 | 64 | 65 | if (!$collectionApiSecret) { 66 | $collectionApiSecret = MomoApi::getCollectionApiSecret(); 67 | } 68 | $this->_collectionApiSecret = $collectionApiSecret; 69 | 70 | 71 | if (!$collectionPrimaryKey) { 72 | $collectionPrimaryKey = MomoApi::getCollectionPrimaryKey(); 73 | } 74 | $this->_collectionPrimaryKey = $collectionPrimaryKey; 75 | 76 | 77 | if (!$collectionUserId) { 78 | $collectionUserId = MomoApi::getCollectionUserId(); 79 | } 80 | $this->_collectionUserId = $collectionUserId; 81 | } 82 | 83 | 84 | /** 85 | * @param array|null $params 86 | * @param array|string|null $options 87 | * 88 | * @return AccessToken The OAuth Token. 89 | */ 90 | public function getToken($params = null, $options = null) 91 | { 92 | 93 | 94 | $url = $this->_baseUrl . '/collection/token/'; 95 | 96 | 97 | $encodedString = base64_encode( 98 | MomoApi::getCollectionUserId() . ':' . MomoApi::getCollectionApiSecret() 99 | ); 100 | $headers = [ 101 | 'Authorization' => 'Basic ' . $encodedString, 102 | 'Content-Type' => 'application/json', 103 | 'Ocp-Apim-Subscription-Key' => MomoApi::getCollectionPrimaryKey() 104 | ]; 105 | 106 | 107 | $response = self::request('post', $url, $params, $headers); 108 | 109 | 110 | $obj = ResourceFactory::accessTokenFromJson($response->json); 111 | 112 | return $obj; 113 | } 114 | 115 | 116 | /** 117 | * @param array|null $params 118 | * @param array|string|null $options 119 | * 120 | * @return Balance The account balance. 121 | */ 122 | public function getBalance($params = null, $options = null) 123 | { 124 | 125 | $url = $this->_baseUrl . "/collection/v1_0/account/balance"; 126 | 127 | $token = $this->getToken()->getToken(); 128 | 129 | 130 | $headers = [ 131 | 'Authorization' => 'Bearer ' . $token, 132 | 'Content-Type' => 'application/json', 133 | "X-Target-Environment" => $this->_targetEnvironment, 134 | 'Ocp-Apim-Subscription-Key' => MomoApi::getCollectionPrimaryKey() 135 | ]; 136 | 137 | 138 | $response = self::request('get', $url, $params, $headers); 139 | 140 | return $response; 141 | } 142 | 143 | 144 | /** 145 | * @param array|null $params 146 | * @param array|string|null $options 147 | * 148 | * @return Transaction The transaction. 149 | */ 150 | public function getTransaction($trasaction_id, $params = null) 151 | { 152 | $url = $this->_baseUrl . "/collection/v1_0/requesttopay/" . $trasaction_id; 153 | 154 | $token = $this->getToken()->getToken(); 155 | 156 | $headers = [ 157 | 'Authorization' => 'Bearer ' . $token, 158 | 'Content-Type' => 'application/json', 159 | "X-Target-Environment" => $this->_targetEnvironment, 160 | 'Ocp-Apim-Subscription-Key' => MomoApi::getCollectionPrimaryKey(), 161 | ]; 162 | 163 | $response = self::request('get', $url, $params, $headers); 164 | 165 | $obj = ResourceFactory::requestToPayFromJson($response->json); 166 | 167 | return $obj; 168 | } 169 | 170 | 171 | /** 172 | * @param array|null $params 173 | * @param array|string|null $options 174 | * 175 | * @return Charge The refunded charge. 176 | */ 177 | public function requestToPay($params, $options = null) 178 | { 179 | 180 | 181 | self::_validateParams($params); 182 | $url = $this->_baseUrl . "/collection/v1_0/requesttopay"; 183 | 184 | $token = $this->getToken()->getToken(); 185 | 186 | $transaction = Util\Util::uuid(); 187 | 188 | $headers = [ 189 | 'Authorization' => 'Bearer ' . $token, 190 | 'Content-Type' => 'application/json', 191 | "X-Target-Environment" => $this->_targetEnvironment, 192 | 'Ocp-Apim-Subscription-Key' => MomoApi::getCollectionPrimaryKey(), 193 | "X-Reference-Id" => $transaction 194 | ]; 195 | 196 | 197 | $data = [ 198 | "payer" => [ 199 | "partyIdType" => "MSISDN", 200 | "partyId" => $params['mobile']], 201 | "payeeNote" => $params['payee_note'], 202 | "payerMessage" => $params['payer_message'], 203 | "externalId" => $params['external_id'], 204 | "currency" => $params['currency'], 205 | "amount" => $params['amount']]; 206 | 207 | 208 | $response = self::request('post', $url, $data, $headers); 209 | 210 | 211 | return $transaction; 212 | } 213 | 214 | 215 | public function isActive($mobile, $params = []) 216 | { 217 | 218 | $token = $this->getToken()->getToken(); 219 | 220 | 221 | $headers = [ 222 | 'Authorization' => 'Bearer ' . $token, 223 | 'Content-Type' => 'application/json', 224 | "X-Target-Environment" => $this->_targetEnvironment, 225 | 'Ocp-Apim-Subscription-Key' => MomoApi::getCollectionPrimaryKey() 226 | ]; 227 | 228 | $url = $this->_baseUrl . "/collection/v1_0/accountholder/MSISDN/" . $mobile . "/active"; 229 | 230 | $response = self::request('get', $url, $params, $headers); 231 | 232 | return $response; 233 | } 234 | 235 | 236 | /** 237 | * @param array|null|mixed $params The list of parameters to validate 238 | * 239 | * @throws \MomoApi\Error\MomoApiError if $params exists and is not an array 240 | */ 241 | protected static function _validateParams($params = null) 242 | { 243 | if ($params && !is_array($params)) { 244 | $message = "You must pass an array as the first argument to MomoApi API " 245 | . "method calls. (HINT: an example call to create a charge " 246 | . "would be: \"MomoApi\\Charge::create(['amount' => 100, " 247 | . "'currency' => 'usd', 'source' => 'tok_1234'])\")"; 248 | throw new \MomoApi\Error\MomoApiError($message); 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /lib/Remittance.php: -------------------------------------------------------------------------------- 1 | _currency = $currency; 57 | 58 | 59 | if (!$baseUrl) { 60 | $baseUrl = MomoApi::getBaseUrl(); 61 | } 62 | $this->_baseUrl = $baseUrl; 63 | 64 | 65 | if (!$targetEnvironment) { 66 | $targetEnvironment = MomoApi::getTargetEnvironment(); 67 | } 68 | $this->_targetEnvironment = $targetEnvironment; 69 | 70 | 71 | if (!$remittanceApiSecret) { 72 | $remittanceApiSecret = MomoApi::getRemittanceApiSecret(); 73 | } 74 | $this->_remittanceApiSecret = $remittanceApiSecret; 75 | 76 | 77 | if (!$remittancePrimaryKey) { 78 | $remittancePrimaryKey = MomoApi::getRemittancePrimaryKey(); 79 | } 80 | $this->_remittancePrimaryKey = $remittancePrimaryKey; 81 | 82 | 83 | if (!$remittanceUserId) { 84 | $remittanceUserId = MomoApi::getRemittanceUserId(); 85 | } 86 | $this->_remittanceUserId = $remittanceUserId; 87 | } 88 | 89 | 90 | /** 91 | * @param array|null $params 92 | * @param array|string|null $options 93 | * 94 | * @return AccessToken The OAuth Token. 95 | */ 96 | public function getToken($params = null, $options = null) 97 | { 98 | $url = $this->_baseUrl . '/remittance/token/'; 99 | 100 | 101 | $encodedString = base64_encode( 102 | MomoApi::getRemittanceUserId() . ':' . MomoApi::getRemittanceApiSecret() 103 | ); 104 | $headers = [ 105 | 'Authorization' => 'Basic ' . $encodedString, 106 | 'Content-Type' => 'application/json', 107 | 'Ocp-Apim-Subscription-Key' => MomoApi::getRemittancePrimaryKey() 108 | ]; 109 | 110 | $response = self::request('post', $url, $params, $headers); 111 | 112 | $obj = ResourceFactory::accessTokenFromJson($response->json); 113 | 114 | return $obj; 115 | } 116 | 117 | 118 | /** 119 | * @param array|null $params 120 | * @param array|string|null $options 121 | * 122 | * @return Balance The account balance. 123 | */ 124 | public function getBalance($params = null, $options = null) 125 | { 126 | $url = $this->_baseUrl . "/remittance/v1_0/account/balance"; 127 | 128 | $token = $this->getToken()->getToken(); 129 | 130 | $headers = [ 131 | 'Authorization' => 'Bearer ' . $token, 132 | 'Content-Type' => 'application/json', 133 | "X-Target-Environment" => $this->_targetEnvironment, 134 | 'Ocp-Apim-Subscription-Key' => MomoApi::getRemittancePrimaryKey() 135 | ]; 136 | 137 | 138 | $response = self::request('get', $url, $params, $headers); 139 | 140 | return $response; 141 | 142 | 143 | $obj = ResourceFactory::balanceFromJson($response->json); 144 | 145 | return $obj; 146 | } 147 | 148 | 149 | /** 150 | * @param array|null $params 151 | * @param array|string|null $options 152 | * 153 | * @return Transaction The transaction. 154 | */ 155 | public function getTransaction($trasaction_id, $params = null) 156 | { 157 | $url = $this->_baseUrl . "/remittance/v1_0/transfer/" . $trasaction_id; 158 | 159 | $token = $this->getToken()->getToken(); 160 | 161 | $headers = [ 162 | 'Authorization' => 'Bearer ' . $token, 163 | 'Content-Type' => 'application/json', 164 | "X-Target-Environment" => $this->_targetEnvironment, 165 | 'Ocp-Apim-Subscription-Key' => MomoApi::getRemittancePrimaryKey(), 166 | ]; 167 | 168 | $response = self::request('get', $url, $params, $headers); 169 | 170 | $obj = ResourceFactory::transferFromJson($response->json); 171 | 172 | return $obj; 173 | } 174 | 175 | 176 | /** 177 | * @param array|null $params 178 | * @param array|string|null $options 179 | * 180 | * @return Charge The refunded charge. 181 | */ 182 | public function transfer($params, $options = null) 183 | { 184 | self::_validateParams($params); 185 | $url = $this->_baseUrl . "/remittance/v1_0/transfer"; 186 | 187 | $token = $this->getToken()->getToken(); 188 | 189 | $transaction = Util\Util::uuid(); 190 | 191 | $headers = [ 192 | 'Authorization' => 'Bearer ' . $token, 193 | 'Content-Type' => 'application/json', 194 | "X-Target-Environment" => $this->_targetEnvironment, 195 | 'Ocp-Apim-Subscription-Key' => MomoApi::getRemittancePrimaryKey(), 196 | "X-Reference-Id" => $transaction 197 | ]; 198 | 199 | $data = [ 200 | "payee" => [ 201 | "partyIdType" => "MSISDN", 202 | "partyId" => $params['mobile']], 203 | "payeeNote" => $params['payee_note'], 204 | "payerMessage" => $params['payer_message'], 205 | "externalId" => $params['external_id'], 206 | "currency" => $params['currency'], 207 | "amount" => $params['amount']]; 208 | 209 | 210 | $response = self::request('post', $url, $data, $headers); 211 | 212 | return $transaction; 213 | } 214 | 215 | 216 | public function isActive($mobile, $params = null) 217 | { 218 | 219 | $token = $this->getToken()->getToken(); 220 | 221 | 222 | $headers = [ 223 | 'Authorization' => 'Bearer ' . $token, 224 | 'Content-Type' => 'application/json', 225 | "X-Target-Environment" => $this->_targetEnvironment, 226 | 'Ocp-Apim-Subscription-Key' => MomoApi::getRemittancePrimaryKey() 227 | ]; 228 | 229 | 230 | $url = $this->_baseUrl . "/remittance/v1_0/accountholder/MSISDN/" . $mobile . "/active"; 231 | 232 | 233 | $response = self::request('get', $url, $params, $headers); 234 | 235 | return $response; 236 | } 237 | 238 | /** 239 | * @param array|null|mixed $params The list of parameters to validate 240 | * 241 | * @throws \MomoApi\Error\MomoApiError if $params exists and is not an array 242 | */ 243 | protected static function _validateParams($params = null) 244 | { 245 | if ($params && !is_array($params)) { 246 | $message = "You must pass an array as the first argument to MomoApi API " 247 | . "method calls. (HINT: an example call to create a charge " 248 | . "would be: \"MomoApi\\Charge::create(['amount' => 100, " 249 | . "'currency' => 'usd', 'source' => 'tok_1234'])\")"; 250 | throw new \MomoApi\Error\MomoApiError($message); 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /lib/Disbursement.php: -------------------------------------------------------------------------------- 1 | _currency = $currency; 56 | 57 | 58 | if (!$baseUrl) { 59 | $baseUrl = MomoApi::getBaseUrl(); 60 | } 61 | $this->_baseUrl = $baseUrl; 62 | 63 | 64 | if (!$targetEnvironment) { 65 | $targetEnvironment = MomoApi::getTargetEnvironment(); 66 | } 67 | $this->_targetEnvironment = $targetEnvironment; 68 | 69 | 70 | if (!$disbursementApiSecret) { 71 | $disbursementApiSecret = MomoApi::getDisbursementApiSecret(); 72 | } 73 | $this->_disbursementApiSecret = $disbursementApiSecret; 74 | 75 | 76 | if (!$disbursementPrimaryKey) { 77 | $disbursementPrimaryKey = MomoApi::getDisbursementPrimaryKey(); 78 | } 79 | $this->_disbursementPrimaryKey = $disbursementPrimaryKey; 80 | 81 | 82 | if (!$disbursementUserId) { 83 | $disbursementUserId = MomoApi::getDisbursementUserId(); 84 | } 85 | $this->_disbursementUserId = $disbursementUserId; 86 | } 87 | 88 | 89 | /** 90 | * @param array|null $params 91 | * @param array|string|null $options 92 | * 93 | * @return AccessToken The OAuth Token. 94 | */ 95 | public function getToken($params = null, $options = null) 96 | { 97 | $url = $this->_baseUrl . '/disbursement/token/'; 98 | 99 | 100 | $encodedString = base64_encode( 101 | MomoApi::getDisbursementUserId() . ':' . MomoApi::getDisbursementApiSecret() 102 | ); 103 | $headers = [ 104 | 'Authorization' => 'Basic ' . $encodedString, 105 | 'Content-Type' => 'application/json', 106 | 'Ocp-Apim-Subscription-Key' => MomoApi::getDisbursementPrimaryKey() 107 | ]; 108 | 109 | 110 | $response = self::request('post', $url, $params, $headers); 111 | 112 | 113 | $obj = ResourceFactory::accessTokenFromJson($response->json); 114 | 115 | return $obj; 116 | } 117 | 118 | 119 | /** 120 | * @param array|null $params 121 | * @param array|string|null $options 122 | * 123 | * @return Balance The account balance. 124 | */ 125 | public function getBalance($params = null, $options = null) 126 | { 127 | $url = $this->_baseUrl . "/disbursement/v1_0/account/balance"; 128 | 129 | $token = $this->getToken()->getToken(); 130 | 131 | 132 | $headers = [ 133 | 'Authorization' => 'Bearer ' . $token, 134 | 'Content-Type' => 'application/json', 135 | "X-Target-Environment" => $this->_targetEnvironment, 136 | 'Ocp-Apim-Subscription-Key' => MomoApi::getDisbursementPrimaryKey() 137 | ]; 138 | 139 | 140 | $response = self::request('get', $url, $params, $headers); 141 | 142 | return $response; 143 | 144 | 145 | $obj = ResourceFactory::balanceFromJson($response->json); 146 | 147 | return $obj; 148 | } 149 | 150 | 151 | /** 152 | * @param array|null $params 153 | * @param array|string|null $options 154 | * 155 | * @return Transaction The transaction. 156 | */ 157 | public function getTransaction($trasaction_id, $params = null) 158 | { 159 | $url = $this->_baseUrl . "/disbursement/v1_0/transfer/" . $trasaction_id; 160 | 161 | $token = $this->getToken()->getToken(); 162 | 163 | $headers = [ 164 | 'Authorization' => 'Bearer ' . $token, 165 | 'Content-Type' => 'application/json', 166 | "X-Target-Environment" => $this->_targetEnvironment, 167 | 'Ocp-Apim-Subscription-Key' => MomoApi::getDisbursementPrimaryKey(), 168 | ]; 169 | 170 | $response = self::request('get', $url, $params, $headers); 171 | 172 | $obj = ResourceFactory::transferFromJson($response->json); 173 | 174 | return $obj; 175 | } 176 | 177 | 178 | /** 179 | * @param array|null $params 180 | * @param array|string|null $options 181 | * 182 | * @return Charge The refunded charge. 183 | */ 184 | public function transfer($params, $options = null) 185 | { 186 | self::_validateParams($params); 187 | $url = $this->_baseUrl . "/disbursement/v1_0/transfer"; 188 | 189 | $token = $this->getToken()->getToken(); 190 | 191 | $transaction = Util\Util::uuid(); 192 | 193 | $headers = [ 194 | 'Authorization' => 'Bearer ' . $token, 195 | 'Content-Type' => 'application/json', 196 | "X-Target-Environment" => $this->_targetEnvironment, 197 | 'Ocp-Apim-Subscription-Key' => MomoApi::getDisbursementPrimaryKey(), 198 | "X-Reference-Id" => $transaction 199 | ]; 200 | 201 | 202 | $data = [ 203 | "payee" => [ 204 | "partyIdType" => "MSISDN", 205 | "partyId" => $params['mobile']], 206 | "payeeNote" => $params['payee_note'], 207 | "payerMessage" => $params['payer_message'], 208 | "externalId" => $params['external_id'], 209 | "currency" => $params['currency'], 210 | "amount" => $params['amount']]; 211 | 212 | 213 | $response = self::request('post', $url, $data, $headers); 214 | 215 | 216 | return $transaction; 217 | } 218 | 219 | 220 | public function isActive($mobile, $params = null) 221 | { 222 | $token = $this->getToken()->getToken(); 223 | 224 | 225 | $headers = [ 226 | 'Authorization' => 'Bearer ' . $token, 227 | 'Content-Type' => 'application/json', 228 | "X-Target-Environment" => $this->_targetEnvironment, 229 | 'Ocp-Apim-Subscription-Key' => MomoApi::getDisbursementPrimaryKey() 230 | ]; 231 | 232 | 233 | $url = $this->_baseUrl . "/disbursement/v1_0/accountholder/MSISDN/" . $mobile . "/active"; 234 | 235 | 236 | $response = self::request('get', $url, $params, $headers); 237 | 238 | return $response; 239 | } 240 | 241 | 242 | /** 243 | * @param array|null|mixed $params The list of parameters to validate 244 | * 245 | * @throws \MomoApi\Error\MomoApiError if $params exists and is not an array 246 | */ 247 | protected static function _validateParams($params = null) 248 | { 249 | if ($params && !is_array($params)) { 250 | $message = "You must pass an array as the first argument to MomoApi API " 251 | . "method calls. (HINT: an example call to create a charge " 252 | . "would be: \"MomoApi\\Charge::create(['amount' => 100, " 253 | . "'currency' => 'usd', 'source' => 'tok_1234'])\")"; 254 | throw new \MomoApi\Error\MomoApiError($message); 255 | } 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /lib/Util/Util.php: -------------------------------------------------------------------------------- 1 | id; 105 | } elseif (static::isList($h)) { 106 | $results = []; 107 | foreach ($h as $v) { 108 | array_push($results, static::objectsToIds($v)); 109 | } 110 | return $results; 111 | } elseif (is_array($h)) { 112 | $results = []; 113 | foreach ($h as $k => $v) { 114 | if (is_null($v)) { 115 | continue; 116 | } 117 | $results[$k] = static::objectsToIds($v); 118 | } 119 | return $results; 120 | } else { 121 | return $h; 122 | } 123 | } 124 | 125 | /** 126 | * @param array $params 127 | * 128 | * @return string 129 | */ 130 | public static function encodeParameters($params) 131 | { 132 | $flattenedParams = self::flattenParams($params); 133 | $pieces = []; 134 | foreach ($flattenedParams as $param) { 135 | list($k, $v) = $param; 136 | array_push($pieces, self::urlEncode($k) . '=' . self::urlEncode($v)); 137 | } 138 | return implode('&', $pieces); 139 | } 140 | 141 | /** 142 | * @param array $params 143 | * @param string|null $parentKey 144 | * 145 | * @return array 146 | */ 147 | public static function flattenParams($params, $parentKey = null) 148 | { 149 | $result = []; 150 | 151 | foreach ($params as $key => $value) { 152 | $calculatedKey = $parentKey ? "{$parentKey}[{$key}]" : $key; 153 | 154 | if (self::isList($value)) { 155 | $result = array_merge($result, self::flattenParamsList($value, $calculatedKey)); 156 | } elseif (is_array($value)) { 157 | $result = array_merge($result, self::flattenParams($value, $calculatedKey)); 158 | } else { 159 | array_push($result, [$calculatedKey, $value]); 160 | } 161 | } 162 | 163 | return $result; 164 | } 165 | 166 | /** 167 | * @param array $value 168 | * @param string $calculatedKey 169 | * 170 | * @return array 171 | */ 172 | public static function flattenParamsList($value, $calculatedKey) 173 | { 174 | $result = []; 175 | 176 | foreach ($value as $i => $elem) { 177 | if (self::isList($elem)) { 178 | $result = array_merge($result, self::flattenParamsList($elem, $calculatedKey)); 179 | } elseif (is_array($elem)) { 180 | $result = array_merge($result, self::flattenParams($elem, "{$calculatedKey}[{$i}]")); 181 | } else { 182 | array_push($result, ["{$calculatedKey}[{$i}]", $elem]); 183 | } 184 | } 185 | 186 | return $result; 187 | } 188 | 189 | /** 190 | * @param string $key A string to URL-encode. 191 | * 192 | * @return string The URL-encoded string. 193 | */ 194 | public static function urlEncode($key) 195 | { 196 | $s = urlencode($key); 197 | 198 | // Don't use strict form encoding by changing the square bracket control 199 | // characters back to their literals. This is fine by the server, and 200 | // makes these parameter strings easier to read. 201 | $s = str_replace('%5B', '[', $s); 202 | $s = str_replace('%5D', ']', $s); 203 | 204 | return $s; 205 | } 206 | 207 | public static function normalizeId($id) 208 | { 209 | if (is_array($id)) { 210 | $params = $id; 211 | $id = $params['id']; 212 | unset($params['id']); 213 | } else { 214 | $params = []; 215 | } 216 | return [$id, $params]; 217 | } 218 | 219 | /** 220 | * Returns UNIX timestamp in milliseconds 221 | * 222 | * @return integer current time in millis 223 | */ 224 | public static function currentTimeMillis() 225 | { 226 | return (int) round(microtime(true) * 1000); 227 | } 228 | 229 | 230 | /** 231 | * Returns a UUID4 string. 232 | * 233 | * @return string uuid4 234 | */ 235 | public static function uuid() 236 | { 237 | return sprintf( 238 | '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', 239 | mt_rand(0, 0xffff), 240 | mt_rand(0, 0xffff), 241 | mt_rand(0, 0xffff), 242 | mt_rand(0, 0x0fff) | 0x4000, 243 | mt_rand(0, 0x3fff) | 0x8000, 244 | mt_rand(0, 0xffff), 245 | mt_rand(0, 0xffff), 246 | mt_rand(0, 0xffff) 247 | ); 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /lib/ApiRequest.php: -------------------------------------------------------------------------------- 1 | _currency = $currency; 75 | } 76 | 77 | 78 | /** 79 | * @return string The base URL. 80 | */ 81 | public static function baseUrl() 82 | { 83 | return MomoApi::$baseUrl; 84 | } 85 | 86 | 87 | /** 88 | * @static 89 | * 90 | * @param ApiResource|bool|array|mixed $d 91 | * 92 | * @return ApiResource|array|string|mixed 93 | */ 94 | private static function _encodeObjects($d) 95 | { 96 | if ($d instanceof ApiResource) { 97 | return Util\Util::utf8($d->id); 98 | } elseif ($d === true) { 99 | return 'true'; 100 | } elseif ($d === false) { 101 | return 'false'; 102 | } elseif (is_array($d)) { 103 | $res = []; 104 | foreach ($d as $k => $v) { 105 | $res[$k] = self::_encodeObjects($v); 106 | } 107 | return $res; 108 | } else { 109 | return Util\Util::utf8($d); 110 | } 111 | } 112 | 113 | /** 114 | * @param string $method 115 | * @param string $url 116 | * @param array|null $params 117 | * @param array|null $headers 118 | * 119 | * @return array An array whose first element is an API response and second 120 | * element is the API key used to make the request. 121 | * @throws Error\MomoApiError 122 | * @throws Error\ApiConnection 123 | */ 124 | public function request($method, $url, $params = null, $headers = null) 125 | { 126 | $params = $params ?: []; 127 | $headers = $headers ?: []; 128 | 129 | 130 | $rawHeaders = []; 131 | 132 | foreach ($headers as $header => $value) { 133 | $rawHeaders[] = $header . ': ' . $value; 134 | } 135 | 136 | 137 | list($rbody, $rcode, $rheaders) = $this->httpClient()->request( 138 | $method, 139 | $url, 140 | $rawHeaders, 141 | $params 142 | ); 143 | 144 | 145 | $json = $this->_interpretResponse($rbody, $rcode, $rheaders); 146 | $resp = new ApiResponse($rbody, $rcode, $rheaders, $json); 147 | return $resp; 148 | } 149 | 150 | /** 151 | * @param string $rbody A JSON string. 152 | * @param int $rcode 153 | * @param array $rheaders 154 | * @param array $resp 155 | */ 156 | public function handleErrorResponse($rbody, $rcode, $rheaders, $resp) 157 | { 158 | if (!is_array($resp) || !isset($resp['error'])) { 159 | $msg = "Invalid response object from API: $rbody " 160 | . "(HTTP response code was $rcode)"; 161 | throw new Error\MomoApiError($msg, $rcode, $rbody, $resp, $rheaders); 162 | } 163 | 164 | $errorData = $resp['error']; 165 | 166 | $error = null; 167 | if (is_string($errorData)) { 168 | $error = self::_specificOAuthError($rbody, $rcode, $rheaders, $resp, $errorData); 169 | } 170 | if (!$error) { 171 | $error = self::_specificAPIError($rbody, $rcode, $rheaders, $resp, $errorData); 172 | } 173 | 174 | throw $error; 175 | } 176 | 177 | /** 178 | * @static 179 | * 180 | * @param string $rbody 181 | * @param int $rcode 182 | * @param array $rheaders 183 | * @param array $resp 184 | * @param array $errorData 185 | * 186 | * @return MomoApiError 187 | */ 188 | private static function _specificAPIError($rbody, $rcode, $rheaders, $resp, $errorData) 189 | { 190 | $msg = isset($errorData['message']) ? $errorData['message'] : null; 191 | $param = isset($errorData['param']) ? $errorData['param'] : null; 192 | $code = isset($errorData['code']) ? $errorData['code'] : null; 193 | $type = isset($errorData['type']) ? $errorData['type'] : null; 194 | 195 | switch ($rcode) { 196 | case 400: 197 | // 'rate_limit' code is deprecated, but left here for backwards compatibility 198 | // for API versions earlier than 2015-09-08 199 | if ($code == 'rate_limit') { 200 | return new Error\RateLimit($msg, $param, $rcode, $rbody, $resp, $rheaders); 201 | } 202 | if ($type == 'idempotency_error') { 203 | return new Error\Idempotency($msg, $rcode, $rbody, $resp, $rheaders); 204 | } 205 | 206 | // intentional fall-through 207 | case 404: 208 | return new Error\InvalidRequest($msg, $param, $rcode, $rbody, $resp, $rheaders); 209 | case 401: 210 | return new Error\Authentication($msg, $rcode, $rbody, $resp, $rheaders); 211 | 212 | default: 213 | return new Error\MomoApiError($msg, $rcode, $rbody, $resp, $rheaders); 214 | } 215 | } 216 | 217 | 218 | /** 219 | * @static 220 | * 221 | * @param null|array $appInfo 222 | * 223 | * @return null|string 224 | */ 225 | private static function _formatAppInfo($appInfo) 226 | { 227 | if ($appInfo !== null) { 228 | $string = $appInfo['name']; 229 | if ($appInfo['version'] !== null) { 230 | $string .= '/' . $appInfo['version']; 231 | } 232 | if ($appInfo['url'] !== null) { 233 | $string .= ' (' . $appInfo['url'] . ')'; 234 | } 235 | return $string; 236 | } else { 237 | return null; 238 | } 239 | } 240 | 241 | 242 | /** 243 | * @param string $rbody 244 | * @param int $rcode 245 | * @param array $rheaders 246 | * 247 | * @return mixed 248 | * @throws Error\MomoApiError 249 | */ 250 | private function _interpretResponse($rbody, $rcode, $rheaders) 251 | { 252 | $resp = json_decode($rbody, true); 253 | if ($rcode == 202) { 254 | return $resp; 255 | } 256 | $jsonError = json_last_error(); 257 | if ($resp === null && $jsonError !== JSON_ERROR_NONE) { 258 | $msg = "Invalid response body from API: $rbody " 259 | . "(HTTP response code was $rcode, json_last_error() was $jsonError)"; 260 | throw new Error\MomoApiError($msg, $rcode, $rbody); 261 | } 262 | 263 | if ($rcode < 200 || $rcode >= 300) { 264 | $this->handleErrorResponse($rbody, $rcode, $rheaders, $resp); 265 | } 266 | return $resp; 267 | } 268 | 269 | /** 270 | * @static 271 | * 272 | * @param HttpClient\ClientInterface $client 273 | */ 274 | public static function setHttpClient($client) 275 | { 276 | self::$_httpClient = $client; 277 | } 278 | 279 | 280 | /** 281 | * @return HttpClient\ClientInterface 282 | */ 283 | private function httpClient() 284 | { 285 | if (!self::$_httpClient) { 286 | self::$_httpClient = HttpClient\CurlClient::instance(); 287 | } 288 | return self::$_httpClient; 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /lib/MomoApi.php: -------------------------------------------------------------------------------- 1 | 15 | 16 | Power your apps with our MTN MoMo API 17 | 18 |
19 | Join our active, engaged community:
20 | Website 21 | | 22 | Spectrum 23 |

24 |
25 | 26 | [![Build Status](https://travis-ci.com/sparkplug/momoapi-php.svg?branch=master)](https://travis-ci.com/sparkplug/momoapi-php) 27 | [![Latest Stable Version](https://poser.pugx.org/sparkplug/momoapi-php/v/stable.svg)](https://packagist.org/packages/sparkplug/momoapi-php) 28 | [![Total Downloads](https://poser.pugx.org/sparkplug/momoapi-php/downloads.svg)](https://packagist.org/packages/sparkplug/momoapi-php) 29 | [![License](https://poser.pugx.org/sparkplug/momoapi-php/license.svg)](https://packagist.org/packages/sparkplug/momoapi-php) 30 | [![Coverage Status](https://coveralls.io/repos/github/sparkplug/momoapi-php/badge.svg?branch=master)](https://coveralls.io/github/sparkplug/momoapi-php?branch=master) 31 | [![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/momo-api-developers/) 32 | 33 | 34 | 35 | # Installation 36 | 37 | You are required to have PHP 5.4.0 and later. 38 | 39 | ## Composer 40 | 41 | You can install the bindings via [Composer](http://getcomposer.org/). Run the following command: 42 | 43 | ```bash 44 | composer require sparkplug/momoapi-php 45 | ``` 46 | 47 | To use the bindings, use Composer's [autoload](https://getcomposer.org/doc/01-basic-usage.md#autoloading): 48 | 49 | ```php 50 | require_once('vendor/autoload.php'); 51 | ``` 52 | 53 | ## Manual Installation 54 | 55 | If you do not wish to use Composer, you can download the [latest release](https://github.com/sparkplug/momoapi-php/releases). Then, to use the bindings, include the `init.php` file. 56 | 57 | ```php 58 | require_once('/path/to/momoapi-php/init.php'); 59 | ``` 60 | 61 | ## Dependencies 62 | 63 | The bindings require the following extensions in order to work properly: 64 | 65 | - [`curl`](https://secure.php.net/manual/en/book.curl.php), although you can use your own non-cURL client if you prefer 66 | - [`json`](https://secure.php.net/manual/en/book.json.php) 67 | - [`mbstring`](https://secure.php.net/manual/en/book.mbstring.php) (Multibyte String) 68 | 69 | If you use Composer, these dependencies should be handled automatically. If you install manually, you'll want to make sure that these extensions are available. 70 | 71 | # Sandbox Environment 72 | 73 | ## Creating a sandbox environment API user 74 | 75 | Next, we need to get the `User ID` and `User Secret` and to do this we shall need to use the Primary Key for the Product to which we are subscribed, as well as specify a host. The library ships with a commandline application that helps to create sandbox credentials. It assumes you have created an account on `https://momodeveloper.mtn.com` and have your `Ocp-Apim-Subscription-Key`. 76 | 77 | ```bash 78 | ## within the project, on the command line. In this example, our domain is akabbo.ug 79 | $ php vendor/sparkplug/momoapi-php/lib/Provision.php 80 | $ providerCallBackHost: https://akabbo.ug 81 | $ Ocp-Apim-Subscription-Key: f83xx8d8xx6749f19a26e2265aeadbcdeg 82 | ``` 83 | 84 | The `providerCallBackHost` is your callback host and `Ocp-Apim-Subscription-Key` is your API key for the specific product to which you are subscribed. The `API Key` is unique to the product and you will need an `API Key` for each product you use. You should get a response similar to the following: 85 | 86 | ```bash 87 | Here is your User Id and API secret : {'apiKey': 'b0431db58a9b41faa8f5860230xxxxxx', 'UserId': '053c6dea-dd68-xxxx-xxxx-c830dac9f401'} 88 | ``` 89 | 90 | These are the credentials we shall use for the sandbox environment. In production, these credentials are provided for you on the MTN OVA management dashboard after KYC requirements are met. 91 | 92 | 93 | 94 | ## Configuration 95 | 96 | Before we can fully utilize the library, we need to specify global configurations. The global configuration using the requestOpts builder. By default, these are picked from environment variables, 97 | but can be overidden using the MomoApi builder 98 | 99 | * `BASE_URL`: An optional base url to the MTN Momo API. By default the staging base url will be used 100 | * `ENVIRONMENT`: Optional enviroment, either "sandbox" or "production". Default is 'sandbox' 101 | * `CURRENCY`: currency by default its EUR 102 | * `CALLBACK_HOST`: The domain where you webhooks urls are hosted. This is mandatory. 103 | * `COLLECTION_PRIMARY_KEY`: The collections API primary key, 104 | * `COLLECTION_USER_ID`: The collection User Id 105 | * `COLLECTION_API_SECRET`: The Collection API secret 106 | * `REMITTANCE_USER_ID`: The Remittance User ID 107 | * `REMITTANCE_API_SECRET`: The Remittance API Secret 108 | * `REMITTANCE_PRIMARY_KEY`: The Remittance Subscription Key 109 | * `DISBURSEMENT_USER_ID`: The Disbursement User ID 110 | * `DISBURSEMENT_API_SECRET`: The Disbursement API Secret 111 | * `DISBURSEMENT_PRIMARY_KEY`: The Disbursement Primary Key 112 | 113 | Once you have specified the global variables, you can now provide the product-specific variables. Each MoMo API product requires its own authentication details i.e its own `Subscription Key`, `User ID` and `User Secret`, also sometimes refered to as the `API Secret`. As such, we have to configure subscription keys for each product you will be using. 114 | 115 | You will only need to configure the variables for the product(s) you will be using. 116 | 117 | you can also use the MomoApi to globally set the different variables. 118 | 119 | 120 | 121 | ```php 122 | MomoApi::setBaseUrl('base'); 123 | 124 | MomoApi::setTargetEnvironment("targetenv"); 125 | 126 | MomoApi::setCurrency("UGX"); 127 | 128 | MomoApi::setCollectionApiSecret("collection_api_secret"); 129 | 130 | MomoApi::setCollectionPrimaryKey("collection_primary_key"); 131 | 132 | MomoApi::setCollectionUserId("collection_user_id"); 133 | 134 | MomoApi::setRemittanceApiSecret("remittance_api_secret"); 135 | 136 | MomoApi::setRemittancePrimaryKey("remittance_primary_key"); 137 | 138 | MomoApi::setRemittanceUserId("remittance_user_id" ); 139 | 140 | MomoApi::setDisbursementApiSecret("disbursement_api_secret"); 141 | 142 | MomoApi::setDisbursementPrimaryKey("disbursement_primary_key"); 143 | 144 | MomoApi::setDisbursementUserId("disbursement_user_id"); 145 | ``` 146 | 147 | 148 | ## Collections 149 | 150 | The collections client can be created with the following paramaters. Note that the `COLLECTION_USER_ID` and `COLLECTION_API_SECRET` for production are provided on the MTN OVA dashboard; 151 | 152 | * `COLLECTION_PRIMARY_KEY`: Primary Key for the `Collection` product on the developer portal. 153 | * `COLLECTION_USER_ID`: For sandbox, use the one generated with the `mtnmomo` command. 154 | * `COLLECTION_API_SECRET`: For sandbox, use the one generated with the `mtnmomo` command. 155 | 156 | You can create a collection client with the following: 157 | 158 | ```php 159 | $client = Collection(); 160 | ``` 161 | 162 | ### Methods 163 | 164 | 1. `requestToPay`: This operation is used to request a payment from a consumer (Payer). The payer will be asked to authorize the payment. The transaction is executed once the payer has authorized the payment. The transaction will be in status PENDING until it is authorized or declined by the payer or it is timed out by the system. Status of the transaction can be validated by using `getTransactionStatus`. 165 | 166 | 2. `getTransaction`: Retrieve transaction information using the `transactionId` returned by `requestToPay`. You can invoke it at intervals until the transaction fails or succeeds. If the transaction has failed, it will throw an appropriate error. 167 | 168 | 3. `getBalance`: Get the balance of the account. 169 | 170 | 4. `isPayerActive`: check if an account holder is registered and active in the system. 171 | 172 | ### Sample Code 173 | 174 | ```php 175 | 176 | $coll = new Collection($currency = "c..", $baseUrl = "url..", $targetEnvironment = "u...", $collectionApiSecret = "u...", $collectionPrimaryKey = "u...", $collectionUserId = "u..."]); 177 | 178 | $params = ['mobile' => "256782181656", 'payee_note' => "34", 'payer_message' => "12", 'external_id' => "ref", 'currency' => "EUR", 'amount' => "500"]; 179 | 180 | $t = $coll->requestToPay($params); 181 | 182 | $transaction = $coll->getTransaction($t); 183 | 184 | ``` 185 | 186 | ## Disbursement 187 | 188 | The Disbursements client can be created with the following paramaters. Note that the `DISBURSEMENT_USER_ID` and `DISBURSEMENT_API_SECRET` for production are provided on the MTN OVA dashboard; 189 | 190 | * `DISBURSEMENT_PRIMARY_KEY`: Primary Key for the `Disbursement` product on the developer portal. 191 | * `DISBURSEMENT_USER_ID`: For sandbox, use the one generated with the `mtnmomo` command. 192 | * `DISBURSEMENT_API_SECRET`: For sandbox, use the one generated with the `mtnmomo` command. 193 | 194 | You can create a disbursements client with the following 195 | 196 | ```php 197 | 198 | $disbursement = new Disbursement(); 199 | 200 | $params = ['mobile' => "256782181656", 'payee_note' => "34", 'payer_message' => "12", 'external_id' => "ref", 'currency' => "EUR", 'amount' => "500"]; 201 | 202 | $t = $disbursement->requestToPay($params); 203 | 204 | 205 | $transaction = $disbursement->getTransaction($t); 206 | 207 | ``` 208 | 209 | ### Methods 210 | 211 | 1. `transfer`: Used to transfer an amount from the owner’s account to a payee account. Status of the transaction can be validated by using the `getTransactionStatus` method. 212 | 213 | 2. `getTransactionStatus`: Retrieve transaction information using the `transactionId` returned by `transfer`. You can invoke it at intervals until the transaction fails or succeeds. 214 | 215 | 2. `getBalance`: Get your account balance. 216 | 217 | 3. `isPayerActive`: This method is used to check if an account holder is registered and active in the system. 218 | 219 | #### Sample Code 220 | 221 | ```php 222 | 223 | 224 | ``` 225 | 226 | 227 | ## Custom Request Timeouts 228 | 229 | *NOTE:* We do not recommend decreasing the timeout for non-read-only calls , since even if you locally timeout, the request can still complete. 230 | 231 | To modify request timeouts (connect or total, in seconds) you'll need to tell the API client to use a CurlClient other than its default. You'll set the timeouts in that CurlClient. 232 | 233 | ```php 234 | // set up your tweaked Curl client 235 | $curl = new \MomoApi\HttpClient\CurlClient(); 236 | $curl->setTimeout(10); // default is \MomoApi\HttpClient\CurlClient::DEFAULT_TIMEOUT 237 | $curl->setConnectTimeout(5); // default is \MomoApi\HttpClient\CurlClient::DEFAULT_CONNECT_TIMEOUT 238 | 239 | echo $curl->getTimeout(); // 10 240 | echo $curl->getConnectTimeout(); // 5 241 | 242 | // tell MomoApi to use the tweaked client 243 | \MomoApi\ApiRequest::setHttpClient($curl); 244 | 245 | // use the Momo API client as you normally would 246 | ``` 247 | 248 | ## Custom cURL Options (e.g. proxies) 249 | 250 | Need to set a proxy for your requests? Pass in the requisite `CURLOPT_*` array to the CurlClient constructor, using the same syntax as `curl_stopt_array()`. This will set the default cURL options for each HTTP request made by the SDK, though many more common options (e.g. timeouts; see above on how to set those) will be overridden by the client even if set here. 251 | 252 | ```php 253 | // set up your tweaked Curl client 254 | $curl = new \MomoApi\HttpClient\CurlClient([CURLOPT_PROXY => 'proxy.local:80']); 255 | // tell MomoApi to use the tweaked client 256 | \MomoApi\ApiRequest::setHttpClient($curl); 257 | ``` 258 | 259 | Alternately, a callable can be passed to the CurlClient constructor that returns the above array based on request inputs. See `testDefaultOptions()` in `tests/CurlClientTest.php` for an example of this behavior. Note that the callable is called at the beginning of every API request, before the request is sent. 260 | 261 | ### Configuring a Logger 262 | 263 | The library does minimal logging, but it can be configured 264 | with a [`PSR-3` compatible logger][psr3] so that messages 265 | end up there instead of `error_log`: 266 | 267 | ```php 268 | \MomoApi\MomoApi::setLogger($logger); 269 | ``` 270 | 271 | 272 | ### Configuring Automatic Retries 273 | 274 | The library can be configured to automatically retry requests that fail due to 275 | an intermittent network problem: 276 | 277 | ```php 278 | \MomoApi\MomoApi::setMaxNetworkRetries(2); 279 | ``` 280 | 281 | 282 | ## Development 283 | 284 | Get [Composer][composer]. For example, on Mac OS: 285 | 286 | ```bash 287 | brew install composer 288 | ``` 289 | 290 | Install dependencies: 291 | 292 | ```bash 293 | composer install 294 | ``` 295 | 296 | 297 | 298 | Install dependencies as mentioned above (which will resolve [PHPUnit](http://packagist.org/packages/phpunit/phpunit)), then you can run the test suite: 299 | 300 | ```bash 301 | ./vendor/bin/phpunit 302 | ``` 303 | 304 | Or to run an individual test file: 305 | 306 | ```bash 307 | ./vendor/bin/phpunit tests/UtilTest.php 308 | ``` 309 | 310 | 311 | [composer]: https://getcomposer.org/ 312 | [curl]: http://curl.haxx.se/docs/caextract.html 313 | [psr3]: http://www.php-fig.org/psr/psr-3/ 314 | -------------------------------------------------------------------------------- /lib/HttpClient/CurlClient.php: -------------------------------------------------------------------------------- 1 | defaultOptions = $defaultOptions; 58 | $this->randomGenerator = $randomGenerator ?: new Util\RandomGenerator(); 59 | $this->initUserAgentInfo(); 60 | 61 | // TODO: curl_reset requires PHP >= 5.5.0. Once we drop support for PHP 5.4, we can simply 62 | // initialize this to true. 63 | $this->enablePersistentConnections = function_exists('curl_reset'); 64 | 65 | $this->enableHttp2 = $this->canSafelyUseHttp2(); 66 | } 67 | 68 | public function initUserAgentInfo() 69 | { 70 | $curlVersion = curl_version(); 71 | $this->userAgentInfo = [ 72 | 'httplib' => 'curl ' . $curlVersion['version'], 73 | 'ssllib' => $curlVersion['ssl_version'], 74 | ]; 75 | } 76 | 77 | /** 78 | * Indicates whether it is safe to use HTTP/2 or not. 79 | * 80 | * @return boolean 81 | */ 82 | private function canSafelyUseHttp2() 83 | { 84 | // Versions of curl older than 7.60.0 don't respect GOAWAY frames 85 | // (cf. https://github.com/curl/curl/issues/2416), which MomoApi use. 86 | $curlVersion = curl_version()['version']; 87 | return (version_compare($curlVersion, '7.60.0') >= 0); 88 | } 89 | 90 | public static function instance() 91 | { 92 | if (!self::$instance) { 93 | self::$instance = new self(); 94 | } 95 | return self::$instance; 96 | } 97 | 98 | public function __destruct() 99 | { 100 | $this->closeCurlHandle(); 101 | } 102 | 103 | /** 104 | * Closes the curl handle if initialized. Do nothing if already closed. 105 | */ 106 | private function closeCurlHandle() 107 | { 108 | if (!is_null($this->curlHandle)) { 109 | curl_close($this->curlHandle); 110 | $this->curlHandle = null; 111 | } 112 | } 113 | 114 | // USER DEFINED TIMEOUTS 115 | 116 | public function getDefaultOptions() 117 | { 118 | return $this->defaultOptions; 119 | } 120 | 121 | public function getUserAgentInfo() 122 | { 123 | return $this->userAgentInfo; 124 | } 125 | 126 | public function getTimeout() 127 | { 128 | return $this->timeout; 129 | } 130 | 131 | public function setTimeout($seconds) 132 | { 133 | $this->timeout = (int)max($seconds, 0); 134 | return $this; 135 | } 136 | 137 | public function getConnectTimeout() 138 | { 139 | return $this->connectTimeout; 140 | } 141 | 142 | public function setConnectTimeout($seconds) 143 | { 144 | $this->connectTimeout = (int)max($seconds, 0); 145 | return $this; 146 | } 147 | 148 | public function request($method, $absUrl, $headers, $params) 149 | { 150 | $method = strtolower($method); 151 | 152 | $opts = []; 153 | if (is_callable($this->defaultOptions)) { // call defaultOptions callback, set options to return value 154 | $opts = call_user_func_array($this->defaultOptions, func_get_args()); 155 | if (!is_array($opts)) { 156 | throw new Error\MomoApiError("Non-array value returned by defaultOptions CurlClient callback"); 157 | } 158 | } elseif (is_array($this->defaultOptions)) { // set default curlopts from array 159 | $opts = $this->defaultOptions; 160 | } 161 | 162 | $params = Util\Util::objectsToIds($params); 163 | 164 | if ($method == 'get') { 165 | $opts[CURLOPT_HTTPGET] = 1; 166 | if (count($params) > 0) { 167 | $encoded = Util\Util::encodeParameters($params); 168 | $absUrl = "$absUrl?$encoded"; 169 | } 170 | } elseif ($method == 'post') { 171 | $opts[CURLOPT_POST] = true; 172 | $opts[CURLOPT_POSTFIELDS] = json_encode($params); 173 | } elseif ($method == 'delete') { 174 | $opts[CURLOPT_CUSTOMREQUEST] = 'DELETE'; 175 | if (count($params) > 0) { 176 | $encoded = json_encode($params); 177 | $absUrl = "$absUrl?$encoded"; 178 | } 179 | } else { 180 | throw new Error\MomoApiError("Unrecognized method $method"); 181 | } 182 | 183 | 184 | // Create a callback to capture HTTP headers for the response 185 | $rheaders = new Util\CaseInsensitiveArray(); 186 | $headerCallback = function ($curl, $header_line) use (&$rheaders) { 187 | // Ignore the HTTP request line (HTTP/1.1 200 OK) 188 | if (strpos($header_line, ":") === false) { 189 | return strlen($header_line); 190 | } 191 | list($key, $value) = explode(":", trim($header_line), 2); 192 | $rheaders[trim($key)] = trim($value); 193 | return strlen($header_line); 194 | }; 195 | 196 | // By default for large request body sizes (> 1024 bytes), cURL will 197 | // send a request without a body and with a `Expect: 100-continue` 198 | // header, which gives the server a chance to respond with an error 199 | // status code in cases where one can be determined right away (say 200 | // on an authentication problem for example), and saves the "large" 201 | // request body from being ever sent. 202 | // 203 | // Unfortunately, the bindings don't currently correctly handle the 204 | // success case (in which the server sends back a 100 CONTINUE), so 205 | // we'll error under that condition. To compensate for that problem 206 | // for the time being, override cURL's behavior by simply always 207 | // sending an empty `Expect:` header. 208 | array_push($headers, 'Expect: '); 209 | 210 | $absUrl = Util\Util::utf8($absUrl); 211 | $opts[CURLOPT_URL] = $absUrl; 212 | $opts[CURLOPT_RETURNTRANSFER] = true; 213 | $opts[CURLOPT_CONNECTTIMEOUT] = $this->connectTimeout; 214 | $opts[CURLOPT_TIMEOUT] = $this->timeout; 215 | $opts[CURLOPT_HEADERFUNCTION] = $headerCallback; 216 | $opts[CURLOPT_HTTPHEADER] = $headers; 217 | 218 | //$opts[CURLOPT_VERBOSE] = 1; 219 | 220 | $opts[CURLOPT_SSL_VERIFYHOST] = false; 221 | $opts[CURLOPT_SSL_VERIFYPEER] = false; 222 | 223 | 224 | if (!isset($opts[CURLOPT_HTTP_VERSION]) && $this->getEnableHttp2()) { 225 | // For HTTPS requests, enable HTTP/2, if supported 226 | $opts[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2TLS; 227 | } 228 | 229 | list($rbody, $rcode) = $this->executeRequestWithRetries($opts, $absUrl); 230 | 231 | return [$rbody, $rcode, $rheaders]; 232 | } 233 | 234 | /** 235 | * @return boolean 236 | */ 237 | public function getEnableHttp2() 238 | { 239 | return $this->enableHttp2; 240 | } 241 | 242 | // END OF USER DEFINED TIMEOUTS 243 | 244 | /** 245 | * @param boolean $enable 246 | */ 247 | public function setEnableHttp2($enable) 248 | { 249 | $this->enableHttp2 = $enable; 250 | } 251 | 252 | /** 253 | * @param array $opts cURL options 254 | */ 255 | private function executeRequestWithRetries($opts, $absUrl) 256 | { 257 | $numRetries = 0; 258 | 259 | while (true) { 260 | $rcode = 0; 261 | $errno = 0; 262 | 263 | $this->resetCurlHandle(); 264 | curl_setopt_array($this->curlHandle, $opts); 265 | $rbody = curl_exec($this->curlHandle); 266 | 267 | if ($rbody === false) { 268 | $errno = curl_errno($this->curlHandle); 269 | $message = curl_error($this->curlHandle); 270 | } else { 271 | $rcode = curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE); 272 | } 273 | if (!$this->getEnablePersistentConnections()) { 274 | $this->closeCurlHandle(); 275 | } 276 | 277 | if ($this->shouldRetry($errno, $rcode, $numRetries)) { 278 | $numRetries += 1; 279 | $sleepSeconds = $this->sleepTime($numRetries); 280 | usleep(intval($sleepSeconds * 1000000)); 281 | } else { 282 | break; 283 | } 284 | } 285 | 286 | if ($rbody === false) { 287 | $this->handleCurlError($absUrl, $errno, $message, $numRetries); 288 | } 289 | 290 | return [$rbody, $rcode]; 291 | } 292 | 293 | /** 294 | * Resets the curl handle. If the handle is not already initialized, or if persistent 295 | * connections are disabled, the handle is reinitialized instead. 296 | */ 297 | private function resetCurlHandle() 298 | { 299 | if (!is_null($this->curlHandle) && $this->getEnablePersistentConnections()) { 300 | curl_reset($this->curlHandle); 301 | } else { 302 | $this->initCurlHandle(); 303 | } 304 | } 305 | 306 | /** 307 | * @return boolean 308 | */ 309 | public function getEnablePersistentConnections() 310 | { 311 | return $this->enablePersistentConnections; 312 | } 313 | 314 | /** 315 | * @param boolean $enable 316 | */ 317 | public function setEnablePersistentConnections($enable) 318 | { 319 | $this->enablePersistentConnections = $enable; 320 | } 321 | 322 | /** 323 | * Initializes the curl handle. If already initialized, the handle is closed first. 324 | */ 325 | private function initCurlHandle() 326 | { 327 | $this->closeCurlHandle(); 328 | $this->curlHandle = curl_init(); 329 | } 330 | 331 | /** 332 | * Checks if an error is a problem that we should retry on. This includes both 333 | * socket errors that may represent an intermittent problem and some special 334 | * HTTP statuses. 335 | * 336 | * @param int $errno 337 | * @param int $rcode 338 | * @param int $numRetries 339 | * @return bool 340 | */ 341 | private function shouldRetry($errno, $rcode, $numRetries) 342 | { 343 | if ($numRetries >= MomoApi::getMaxNetworkRetries()) { 344 | return false; 345 | } 346 | 347 | // Retry on timeout-related problems (either on open or read). 348 | if ($errno === CURLE_OPERATION_TIMEOUTED) { 349 | return true; 350 | } 351 | 352 | // Destination refused the connection, the connection was reset, or a 353 | // variety of other connection failures. This could occur from a single 354 | // saturated server, so retry in case it's intermittent. 355 | if ($errno === CURLE_COULDNT_CONNECT) { 356 | return true; 357 | } 358 | 359 | // 409 conflict 360 | if ($rcode === 409) { 361 | return true; 362 | } 363 | 364 | return false; 365 | } 366 | 367 | private function sleepTime($numRetries) 368 | { 369 | // Apply exponential backoff with $initialNetworkRetryDelay on the 370 | // number of $numRetries so far as inputs. Do not allow the number to exceed 371 | // $maxNetworkRetryDelay. 372 | $sleepSeconds = min( 373 | MomoApi::getInitialNetworkRetryDelay() * 1.0 * pow(2, $numRetries - 1), 374 | MomoApi::getMaxNetworkRetryDelay() 375 | ); 376 | 377 | // Apply some jitter by randomizing the value in the range of 378 | // ($sleepSeconds / 2) to ($sleepSeconds). 379 | $sleepSeconds *= 0.5 * (1 + $this->randomGenerator->randFloat()); 380 | 381 | // But never sleep less than the base sleep seconds. 382 | $sleepSeconds = max(MomoApi::getInitialNetworkRetryDelay(), $sleepSeconds); 383 | 384 | return $sleepSeconds; 385 | } 386 | 387 | /** 388 | * @param string $url 389 | * @param int $errno 390 | * @param string $message 391 | * @param int $numRetries 392 | * @throws Error\ApiConnection 393 | */ 394 | private function handleCurlError($url, $errno, $message, $numRetries) 395 | { 396 | switch ($errno) { 397 | case CURLE_COULDNT_CONNECT: 398 | case CURLE_COULDNT_RESOLVE_HOST: 399 | case CURLE_OPERATION_TIMEOUTED: 400 | $msg = "Could not connect to MomoApi ($url). Please check your " 401 | . "internet connection and try again. If this problem persists, " 402 | . "you should check MomoApi's service status at " 403 | . "https://twitter.com/stripestatus, or"; 404 | break; 405 | case CURLE_SSL_CACERT: 406 | case CURLE_SSL_PEER_CERTIFICATE: 407 | $msg = "Could not verify MomoApi's SSL certificate. Please make sure " 408 | . "that your network is not intercepting certificates. " 409 | . "(Try going to $url in your browser.) " 410 | . "If this problem persists,"; 411 | break; 412 | default: 413 | $msg = "Unexpected error communicating with MomoApi. " 414 | . "If this problem persists,"; 415 | } 416 | $msg .= " let us know at mossplix@gmail.com 417 | 418 | ."; 419 | 420 | $msg .= "\n\n(Network error [errno $errno]: $message)"; 421 | 422 | if ($numRetries > 0) { 423 | $msg .= "\n\nRequest was retried $numRetries times."; 424 | } 425 | 426 | throw new Error\ApiConnection($msg); 427 | } 428 | 429 | /** 430 | * Checks if a list of headers contains a specific header name. 431 | * 432 | * @param string[] $headers 433 | * @param string $name 434 | * @return boolean 435 | */ 436 | private function hasHeader($headers, $name) 437 | { 438 | foreach ($headers as $header) { 439 | if (strncasecmp($header, "{$name}: ", strlen($name) + 2) === 0) { 440 | return true; 441 | } 442 | } 443 | 444 | return false; 445 | } 446 | } 447 | --------------------------------------------------------------------------------