├── LICENSE ├── Makefile ├── composer.json ├── config └── salesforce.php ├── http └── routes.php ├── migrations └── salesforce.php ├── readme.md ├── resources └── views │ └── salesforce_login.blade.php └── src ├── Authentication.php ├── Bulk.php ├── Client └── BulkClient.php ├── Contracts └── Arrayable.php ├── Controllers ├── BaseController.php └── SalesforceController.php ├── Custom.php ├── DataObjects ├── Attachment.php ├── BaseObject.php ├── BinaryBatch.php └── SalesforceError.php ├── Facades └── Salesforce.php ├── Interfaces └── BulkBatchProcessorInterface.php ├── Models └── SalesforceToken.php ├── Providers └── SalesforceLaravelServiceProvider.php ├── Query.php ├── Repositories ├── Eloquent │ └── TokenEloquentRepository.php ├── TokenRepository.php └── TokenRepositoryInterface.php ├── Responses ├── Bulk │ ├── BulkBatchResponse.php │ ├── BulkBatchResultResponse.php │ └── BulkJobResponse.php ├── Query │ ├── QueryResponse.php │ └── SearchResponse.php ├── SalesforceBaseResponse.php └── Sobject │ ├── SobjectDeleteResponse.php │ ├── SobjectGetResponse.php │ ├── SobjectInsertResponse.php │ └── SobjectUpdateResponse.php ├── Salesforce.php ├── SalesforceConfig.php ├── Sobject.php └── helpers.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Progyny, Inc & Frank Kessler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | help: 2 | @echo "Please use \`make ' where is one of" 3 | @echo " start-server to start the test server" 4 | @echo " stop-server to stop the test server" 5 | @echo " test to perform unit tests. Provide TEST to perform a specific test." 6 | 7 | start-server: stop-server 8 | node --version && npm --version && node tests/server.js &> /dev/null & 9 | 10 | stop-server: 11 | @PID=$(shell ps axo pid,command \ 12 | | grep 'tests/server.js' \ 13 | | grep -v grep \ 14 | | cut -f 1 -d " "\ 15 | ) && [ -n "$$PID" ] && kill $$PID || true 16 | 17 | test: start-server 18 | vendor/bin/phpunit 19 | $(MAKE) stop-server -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frankkessler/salesforce-laravel-oauth2-rest", 3 | "description": "A Salesforce REST api wrapper utilizing oAuth2", 4 | "keywords": ["salesforce", "rest", "oauth2"], 5 | "license": "MIT", 6 | "type": "project", 7 | "minimum-stability": "dev", 8 | "prefer-stable": true, 9 | "require": { 10 | "php": ">=5.5.9", 11 | "guzzlehttp/guzzle":"~6.0", 12 | "frankkessler/guzzle-oauth2-middleware":"0.1.*", 13 | "illuminate/http": "~5.1", 14 | "illuminate/routing": "~5.1", 15 | "illuminate/support": "~5.1", 16 | "illuminate/database": "~5.1", 17 | "psr/log": "~1.0" 18 | }, 19 | "require-dev": { 20 | "phpunit/phpunit": "~4.0", 21 | "phpspec/phpspec": "~2.1", 22 | "mockery/mockery": "0.9.*", 23 | "satooshi/php-coveralls": "1.*" 24 | }, 25 | "autoload": { 26 | "classmap": [ 27 | "src/Controllers" 28 | ], 29 | "psr-4": { 30 | "Frankkessler\\Salesforce\\": "src" 31 | }, 32 | "files": [ 33 | "src/helpers.php" 34 | ] 35 | }, 36 | "autoload-dev": { 37 | "files": [ 38 | "tests/GuzzleServer.php", 39 | "tests/BulkBatchProcessor.php" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /config/salesforce.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'domain' => env('SALESFORCE_API_DOMAIN', 'na1.salesforce.com'), 6 | 7 | 'base_uri' => env('SALESFORCE_API_BASE_URI', '/services/data/v36.0/'), 8 | 9 | 'version' => env('SALESFORCE_API_BASE_VERSION', '36.0'), 10 | ], 11 | 'oauth' => [ 12 | 'auth_type' => env('SALESFORCE_OAUTH_AUTH_TYPE', 'web_server'), //OPTIONS: web_server or jwt_web_token 13 | 14 | 'domain' => env('SALESFORCE_OAUTH_DOMAIN', 'login.salesforce.com'), 15 | 16 | 'authorize_uri' => env('SALESFORCE_OAUTH_AUTHORIZE_URI', '/services/oauth2/authorize'), 17 | 18 | 'token_uri' => env('SALESFORCE_OAUTH_TOKEN_URI', '/services/oauth2/token'), 19 | 20 | 'callback_url' => env('SALESFORCE_OAUTH_CALLBACK_URL', ''), 21 | 22 | 'consumer_token' => env('SALESFORCE_OAUTH_CONSUMER_TOKEN', null), 23 | 24 | 'consumer_secret' => env('SALESFORCE_OAUTH_CONSUMER_SECRET', null), 25 | 26 | 'scopes' => [ 27 | 'api', 28 | 'offline_access', 29 | 'refresh_token', 30 | ], 31 | 32 | 'jwt' => [ 33 | 'private_key' => env('SALESFORCE_OAUTH_JWT_PRIVATE_KEY'), 34 | 'private_key_passphrase' => env('SALESFORCE_OAUTH_JWT_PRIVATE_KEY_PASSPHRASE'), 35 | 'run_as_user_name' => env('SALESFORCE_OAUTH_JWT_RUN_AS_USER_NAME'), 36 | ], 37 | ], 38 | 'storage_type' => 'eloquent', 39 | 'storage_global_user_id' => null, 40 | 'enable_oauth_routes' => env('SALESFORCE_ENABLE_OAUTH_ROUTES', false), 41 | 'logger' => env('SALESFORCE_LOGGER_CLASS', null), 42 | ]; 43 | -------------------------------------------------------------------------------- /http/routes.php: -------------------------------------------------------------------------------- 1 | 'auth'], function () { 4 | \Route::get('salesforce/admin/login', 'Frankkessler\Salesforce\Controllers\SalesforceController@login_form'); 5 | \Route::get('salesforce/admin/callback', 'Frankkessler\Salesforce\Controllers\SalesforceController@process_authorization_callback'); 6 | \Route::get('salesforce/admin/test', 'Frankkessler\Salesforce\Controllers\SalesforceController@test_account'); 7 | }); 8 | -------------------------------------------------------------------------------- /migrations/salesforce.php: -------------------------------------------------------------------------------- 1 | getConnection())) { 18 | $connection->useDefaultSchemaGrammar(); 19 | } else { 20 | $app = app(); 21 | $connection = $app['db']->connection($this->getConnection()); 22 | } 23 | 24 | $schema = new Schema($connection); 25 | 26 | if (!$schema->hasTable('salesforce_tokens')) { 27 | $schema->create('salesforce_tokens', function (Blueprint $table) { 28 | $table->bigIncrements('id'); 29 | $table->string('access_token'); 30 | $table->string('refresh_token'); 31 | $table->string('instance_base_url'); 32 | $table->bigInteger('user_id'); 33 | $table->datetime('expires')->nullable(); 34 | $table->timestamps(); 35 | $table->softDeletes(); 36 | }); 37 | } 38 | } 39 | 40 | /** 41 | * Reverse the migrations. 42 | * 43 | * @return void 44 | */ 45 | public function down() 46 | { 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![Travis CI Build Status](https://api.travis-ci.org/frankkessler/salesforce-laravel-oauth2-rest.svg)](https://travis-ci.org/frankkessler/salesforce-laravel-oauth2-rest/) 2 | [![Coverage Status](https://coveralls.io/repos/github/frankkessler/salesforce-laravel-oauth2-rest/badge.svg?branch=master)](https://coveralls.io/github/frankkessler/salesforce-laravel-oauth2-rest?branch=master) 3 | [![StyleCI](https://styleci.io/repos/42465034/shield)](https://styleci.io/repos/42465034) 4 | [![Latest Stable Version](https://img.shields.io/packagist/v/frankkessler/salesforce-laravel-oauth2-rest.svg)](https://packagist.org/packages/frankkessler/salesforce-laravel-oauth2-rest) 5 | 6 | # RUNNING UNIT TESTS 7 | 8 | There are currently two ways to run the unit tests. Keep in mind that node is a dependency for running the unit tests regardless of which way you want to run them. 9 | 10 | If you have make installed you can run 11 | ``` 12 | make tests 13 | ``` 14 | 15 | If you would prefer to see more details about how the unit tests run, you can start the node server and then run the unit tests from another window. 16 | ``` 17 | node tests/server.js 8126 true 18 | ``` 19 | Then in a different window: 20 | ``` 21 | vendor/bin/phpunit 22 | ``` 23 | 24 | # INSTALLATION 25 | 26 | To install this package, add the following to your composer.json file 27 | 28 | ```json 29 | "frankkessler/salesforce-laravel-oauth2-rest": "0.4.*" 30 | ``` 31 | 32 | ## LARAVEL 5 SPECIFIC INSTALLATION TASKS 33 | Add the following to your config/app.php file in the providers array 34 | 35 | ```php 36 | Frankkessler\Salesforce\Providers\SalesforceLaravelServiceProvider::class, 37 | ``` 38 | 39 | Add the following to your config/app.php file in the aliases array 40 | 41 | ```php 42 | 'Salesforce' => Frankkessler\Salesforce\Facades\Salesforce::class, 43 | ``` 44 | 45 | Run the following command to pull the project config file and database migration into your project 46 | 47 | ```bash 48 | php artisan vendor:publish 49 | ``` 50 | 51 | Run the migration 52 | 53 | ```bash 54 | php artisan migrate 55 | ``` 56 | 57 | ##OPTIONAL INSTALLATION 58 | 59 | Logging is enabled by default if using Laravel. If not, add the following to the $config parameter when initializing the Salesforce class. (This class must implement the Psr\Log\LoggerInterface interface.) 60 | 61 | ```php 62 | 'salesforce.logger' => $class_or_class_name 63 | ``` 64 | 65 | #TOKEN SETUP 66 | 67 | Currently, this package only supports the web server flow (Authorization Code) and JWT Web Tokens for oauth2. 68 | 69 | 70 | ## Web Server Flow Setup (Authorization Code) 71 | 72 | This utilizes access and refresh tokens in order to grant access. 73 | 74 | To get started, you'll have to setup a Connected App in Salesforce. 75 | 1. Navigate to Setup -> Create -> Apps 76 | 2. In the Connected Apps section, click New 77 | 3. Fill out the form including the Api/Oauth section. 78 | 1. Your callback URL must be https and will follow this format: http://yourdomain.com/salesforce/callback 79 | 4. Save and wait 10 minutes for the settings to save. 80 | 81 | Now that you have your Client Id and Client secret, add them to your .env file: 82 | 83 | ```php 84 | SALESFORCE_API_DOMAIN=na1.salesforce.com 85 | SALESFORCE_OAUTH_CALLBACK_URL=https://yourdomain.com/salesforce/callback 86 | SALESFORCE_OAUTH_DOMAIN=login.salesforce.com 87 | SALESFORCE_OAUTH_CONSUMER_TOKEN=YOUR_CLIENT_ID_FROM_CREATED_APP 88 | SALESFORCE_OAUTH_CONSUMER_SECRET=YOUR_CLIENT_SECRET_FROM_CREATED_APP 89 | ``` 90 | 91 | To login and authorize this application add the following to your routes.php file. You may remove these routes once you have your refresh token stored in the database. 92 | 93 | ```php 94 | Route::get('salesforce/login', '\Frankkessler\Salesforce\Controllers\SalesforceController@login_form'); 95 | Route::get('salesforce/callback', '\Frankkessler\Salesforce\Controllers\SalesforceController@process_authorization_callback'); 96 | ``` 97 | 98 | Visit https://yourdomain.com/salesforce/login to authorize your application. Your access token and refresh token will be stored in the salesforce_tokens database table by default. 99 | 100 | ## JWT Web Token 101 | 102 | Setup your public and private key pair. Salesforce recommends you use an RSA 256 key. Here is a sample script that will generate a key for you in PHP. 103 | 104 | ``` 105 | $privateKeyPassphrase = null; 106 | $csrString = ''; 107 | $privateKeyString = ''; 108 | $certString = ''; 109 | 110 | $config = [ 111 | 'private_key_type' => \OPENSSL_KEYTYPE_RSA, 112 | 'digest_alg' => 'sha256', 113 | 'private_key_bits' => 2048, 114 | ]; 115 | 116 | $dn = array( 117 | "countryName" => "US", 118 | "stateOrProvinceName" => "New York", 119 | "localityName" => "New York", 120 | "organizationName" => "SalesforceLaravel", 121 | "organizationalUnitName" => "SalesforceLaravel", 122 | "commonName" => "SalesforceLaravel", 123 | "emailAddress" => "SalesforceLaravel@example.com" 124 | ); 125 | 126 | $privateKey = openssl_pkey_new($config); 127 | 128 | $csr = openssl_csr_new($dn, $privateKey); 129 | 130 | $sscert = openssl_csr_sign($csr, null, $privateKey, 365); 131 | 132 | openssl_csr_export($csr, $csrString); 133 | file_put_contents(__DIR__.'/../csr.csr', $csrString); 134 | 135 | openssl_x509_export($sscert, $certString); 136 | file_put_contents(__DIR__.'/../public.crt', $certString); 137 | 138 | openssl_pkey_export($privateKey, $privateKeyString, $privateKeyPassphrase); 139 | file_put_contents(__DIR__.'/../private.key', $privateKeyString); 140 | ``` 141 | 142 | Setup your app in Salesforce: 143 | 144 | 1. Navigate to Setup -> Create -> Apps 145 | 2. In the Connected Apps section, click New 146 | 3. Fill out the form including the Api/Oauth section. 147 | 1. Your callback URL must be https and will follow this format: http://yourdomain.com/salesforce/callback 148 | 2. Check off "Use Digital Signature" and upload the PUBLIC key (public.crt) generated in the previous section. 149 | 3. Select the Oauth scopes you want, but make sure you select the refresh_token, offline_access scope or this flow will fail. 150 | 4. Save 151 | 5. To get up and running as quick as possible you can follow the next few steps, but they might not work for everyone. 152 | 6. Navigate to Setup -> Manage Apps -> Connected Apps and click on the App you just created 153 | 7. Click edit and select "Admin approved users are pre-authorized" in the Permitted users field 154 | 8. Click Save 155 | 9. Scroll down on the same page and select the Manage Profiles button to add the profiles that are pre-authorized to use JWT Web Tokens in your app 156 | 157 | 158 | Add the following variables to your .env file. 159 | 160 | ``` 161 | SALESFORCE_API_DOMAIN=na1.salesforce.com 162 | SALESFORCE_OAUTH_CALLBACK_URL=https://yourdomain.com/salesforce/callback 163 | SALESFORCE_OAUTH_DOMAIN=login.salesforce.com 164 | SALESFORCE_OAUTH_CONSUMER_TOKEN=YOUR_CLIENT_ID_FROM_CREATED_APP 165 | SALESFORCE_OAUTH_CONSUMER_SECRET=YOUR_CLIENT_SECRET_FROM_CREATED_APP 166 | SALESFORCE_OAUTH_AUTH_TYPE="jwt_web_token" 167 | SALESFORCE_OAUTH_JWT_PRIVATE_KEY="/usr/path/to/my/private.key" 168 | SALESFORCE_OAUTH_JWT_PRIVATE_KEY_PASSPHRASE="testpassword" //optional 169 | SALESFORCE_OAUTH_JWT_RUN_AS_USER_NAME="test.use@mycompanyname.com" //This is the Salesforce username that will be used to connect 170 | ``` 171 | 172 | # EXAMPLES 173 | 174 | 175 | ### Get a sObject record 176 | 177 | ```php 178 | $objectType = 'Account'; 179 | $objectId = 'OBJECT_ID'; 180 | 181 | $result = Salesforce::sobject()->get($objectId, $objectType); 182 | 183 | if($result->success){ 184 | $name = $result->sobject['Name']; 185 | } 186 | ``` 187 | 188 | ### Insert a sObject record 189 | 190 | ```php 191 | $objectType = 'Account'; 192 | $objectData = [ 193 | 'Name' => 'Acme', 194 | 'Description' => 'Account Description', 195 | ]; 196 | 197 | $result = Salesforce::sobject()->insert($objectType, $objectData); 198 | 199 | if($result->success){ 200 | $id = $result->id; 201 | } 202 | ``` 203 | 204 | ### Update a sObject record 205 | 206 | ```php 207 | $objectType = 'Account'; 208 | $objectId = 'OBJECT_ID'; 209 | $objectData = [ 210 | 'Name' => 'Acme', 211 | 'Description' => 'Account Description', 212 | ]; 213 | 214 | $result = Salesforce::sobject()->update($objectId, $objectType, $objectData); 215 | 216 | if($result->success){ 217 | //no data returned 218 | } 219 | ``` 220 | 221 | ### Delete a sObject record 222 | 223 | ```php 224 | $objectType = 'Account'; 225 | $objectId = 'OBJECT_ID'; 226 | 227 | $result = Salesforce::sobject()->delete($objectId, $objectType); 228 | 229 | if($result->success){ 230 | //no data returned 231 | } 232 | ``` 233 | 234 | ### Perform a SOQL Query 235 | 236 | ```php 237 | $soql = 'SELECT Id, Name FROM Account LIMIT 1'; 238 | 239 | $result = Salesforce::query()->query($soql); 240 | 241 | if($result->success && $result->totalSize > 0){ 242 | foreach($result->records as $record){ 243 | $account_name = $record['Name']; 244 | } 245 | } 246 | ``` 247 | 248 | ### Perform a SOQL Query that will get all the records 249 | 250 | ```php 251 | //since all records are grabbed at once and all records will be in memory, test the max size of the data you would like to use with this function before running in production. Large data sets could throw PHP OUT OF MEMORY errors. 252 | 253 | $soql = 'SELECT Id, Name FROM Account LIMIT 1'; 254 | 255 | $result = Salesforce::query()->queryFollowNext($soql); 256 | 257 | if($result->success && $result->totalSize > 0){ 258 | foreach($result->records as $record){ 259 | $account_name = $record['Name']; 260 | } 261 | } 262 | ``` 263 | 264 | ### Perform a SOSL Query 265 | 266 | ```php 267 | $sosl = 'FIND {Acme} IN ALL FIELDS RETURNING Account(Id, Name ORDER BY LastModifiedDate DESC LIMIT 3)'; 268 | 269 | $result = Salesforce::query()->search($sosl); 270 | 271 | if($result->success && $result->totalSize > 0){ 272 | foreach($result->records as $record){ 273 | $account_name = $record['Name']; 274 | } 275 | } 276 | ``` 277 | 278 | ## Bulk Api Processing 279 | 280 | ### Insert 281 | 282 | ```php 283 | $operationType = 'insert'; 284 | $objectType = 'Account'; 285 | $objectData = [ 286 | [ 287 | 'Name' => 'Acme', 288 | 'Description' => 'Account Description', 289 | ], 290 | [ 291 | 'Name' => 'Acme2', 292 | 'Description' => 'Account Description2', 293 | ], 294 | ]; 295 | 296 | $result = Salesforce::bulk()->runBatch($operationType, $objectType, $objectData); 297 | 298 | if($result->id){ 299 | $id = $result->id; 300 | } 301 | ``` 302 | 303 | ### Upsert 304 | 305 | ```php 306 | $operationType = 'upsert'; 307 | $objectType = 'Account'; 308 | $objectData = [ 309 | [ 310 | 'ExternalId__c' => 'ID1', 311 | 'Name' => 'Acme', 312 | 'Description' => 'Account Description', 313 | ], 314 | [ 315 | 'ExternalId__c' => 'ID2', 316 | 'Name' => 'Acme2', 317 | 'Description' => 'Account Description2', 318 | ], 319 | ]; 320 | 321 | $result = Salesforce::bulk()->runBatch($operationType, $objectType, $objectData, ['externalIdFieldName' => 'ExternalId__c']); 322 | 323 | if($result->id){ 324 | foreach ($result->batches as $batch) { 325 | echo $batch->numberRecordsProcessed; 326 | echo $batch->numberRecordsFailed; 327 | foreach ($batch->records as $record) { 328 | if(!$record['success']){ 329 | echo 'Record Failed: '.json_encode($record); 330 | } 331 | } 332 | } 333 | } 334 | 335 | ``` 336 | 337 | ### Query 338 | 339 | ```php 340 | $operationType = 'query'; 341 | $objectType = 'Account'; 342 | $objectData = 'SELECT Id, Name FROM Account LIMIT 10'; 343 | 344 | $result = Salesforce::bulk()->runBatch($operationType, $objectType, $objectData); 345 | 346 | if ($result->id) { 347 | $id = $result->id; 348 | foreach ($result->batches as $batch) { 349 | foreach ($batch->records as $record) { 350 | $account_id = $record['Id']; 351 | } 352 | } 353 | } 354 | ``` 355 | 356 | ### Query with PK Chunking 357 | 358 | ```php 359 | $operationType = 'query'; 360 | $objectType = 'Account'; 361 | $objectData = 'SELECT Id, Name FROM Account'; 362 | 363 | $result = Salesforce::bulk()->runBatch($operationType, $objectType, $objectData, [ 364 | 'contentType' => 'CSV', 365 | 'Sforce-Enable-PKChunking' => [ 366 | 'chunkSize' => 2500, 367 | ], 368 | ]); 369 | 370 | if ($result->id) { 371 | $id = $result->id; 372 | foreach ($result->batches as $batch) { 373 | foreach ($batch->records as $record) { 374 | $account_id = $record['Id']; 375 | } 376 | } 377 | } 378 | ``` 379 | 380 | ### Query with Class to Process each Batch 381 | 382 | ```php 383 | $operationType = 'query'; 384 | $objectType = 'Account'; 385 | $objectData = 'SELECT Id, Name FROM Account LIMIT 10'; 386 | 387 | $result = Salesforce::bulk()->runBatch($operationType, $objectType, $objectData,[ 388 | 'batchProcessor' => CustomBulkBatchProcessor::class, //Class must implement Frankkessler\Salesforce\Interfaces\BulkBatchProcessorInterface 389 | ]); 390 | 391 | if ($result->id) { 392 | $id = $result->id; 393 | } 394 | ``` 395 | 396 | ### Custom REST Endpoint (GET) 397 | 398 | ```php 399 | $uri = 'custom_apex_uri_get'; 400 | 401 | $result = Salesforce::custom()->get($uri); 402 | 403 | if($result->http_status == 200){ 404 | $body = $result->raw_body; 405 | } 406 | ``` 407 | 408 | ### Custom REST Endpoint (POST) 409 | 410 | ```php 411 | $uri = 'custom_apex_uri_post'; 412 | 413 | $result = Salesforce::custom()->post($uri); 414 | 415 | if($result->http_status == 200){ 416 | //success 417 | } 418 | ``` -------------------------------------------------------------------------------- /resources/views/salesforce_login.blade.php: -------------------------------------------------------------------------------- 1 | {!! \Form::open(array('action' => '\Frankkessler\Salesforce\Controllers\SalesforceController@login_form_submit')) !!} 2 |
3 | {!! Form::label('username', 'Username') !!} 4 | {!! \Form::text('username') !!} 5 |
6 |
7 | {!! Form::label('password', 'Password') !!} 8 | {!! \Form::password('password') !!} 9 |
10 | {!! \Form::submit('Submit') !!} 11 | {!! \Form::close() !!} 12 | -------------------------------------------------------------------------------- /src/Authentication.php: -------------------------------------------------------------------------------- 1 | SalesforceConfig::get('salesforce.oauth.consumer_token'), 19 | 'redirect_uri' => SalesforceConfig::get('salesforce.oauth.callback_url'), 20 | 'scope' => SalesforceConfig::get('salesforce.oauth.scopes'), 21 | ]; 22 | 23 | return 'Login to Salesforce'; 24 | } 25 | 26 | public static function processAuthenticationCode($code, $options = []) 27 | { 28 | $repository = new TokenRepository(); 29 | 30 | $base_uri = 'https://'.SalesforceConfig::get('salesforce.api.domain').SalesforceConfig::get('salesforce.api.base_uri'); 31 | 32 | $oauth2Client = new Oauth2Client(array_replace([ 33 | 'base_uri' => $base_uri, 34 | ], $options)); 35 | 36 | $authorization_config = [ 37 | 'code' => $code, 38 | 'client_id' => SalesforceConfig::get('salesforce.oauth.consumer_token'), 39 | 'client_secret' => SalesforceConfig::get('salesforce.oauth.consumer_secret'), 40 | 'redirect_uri' => SalesforceConfig::get('salesforce.oauth.callback_url'), 41 | 'token_url' => 'https://'.SalesforceConfig::get('salesforce.oauth.domain').SalesforceConfig::get('salesforce.oauth.token_uri'), 42 | 'auth_location' => 'body', 43 | ]; 44 | $oauth2Client->setGrantType(new AuthorizationCode($authorization_config)); 45 | 46 | $refresh_token = ''; 47 | if ($refresh_token) { 48 | $refresh_config = [ 49 | 'refresh_token' => $refresh_token, 50 | 'client_id' => SalesforceConfig::get('salesforce.oauth.consumer_token'), 51 | 'client_secret' => SalesforceConfig::get('salesforce.oauth.consumer_secret'), 52 | ]; 53 | $oauth2Client->setRefreshTokenGrantType(new RefreshToken($refresh_config)); 54 | } 55 | 56 | $access_token = $oauth2Client->getAccessToken(); 57 | 58 | $repository->store->setTokenRecord($access_token); 59 | 60 | return 'Token record set successfully'; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Bulk.php: -------------------------------------------------------------------------------- 1 | $base_uri, 23 | 'auth' => 'bulk', 24 | ]; 25 | 26 | if (isset($config['handler'])) { 27 | $client_config['handler'] = $config['handler']; 28 | } 29 | 30 | $this->oauth2Client = new BulkClient($client_config); 31 | 32 | parent::__construct(array_replace($config, $client_config)); 33 | } 34 | 35 | public function runBatch($operation, $objectType, $data, $options = []) 36 | { 37 | $batches = []; 38 | 39 | $defaults = [ 40 | 'externalIdFieldName' => null, 41 | 'batchSize' => 2000, 42 | 'batchTimeout' => 600, 43 | 'contentType' => 'JSON', 44 | 'pollIntervalSeconds' => 5, 45 | 'isBatchedResult' => false, 46 | 'concurrencyMode' => 'Parallel', 47 | 'Sforce-Enable-PKChunking' => false, 48 | 'batchProcessor' => null, 49 | ]; 50 | 51 | $options = array_replace($defaults, $options); 52 | 53 | if ($operation == 'query') { 54 | $options['isBatchedResult'] = true; 55 | } 56 | 57 | $job = $this->createJob($operation, $objectType, $options['externalIdFieldName'], $options['contentType'], $options['concurrencyMode'], $options); 58 | 59 | if ($job->id) { 60 | //if data is array, we can split it into batches 61 | if (is_array($data)) { 62 | $totalNumberOfBatches = ceil(count($data) / $options['batchSize']); 63 | $this->log('info', 'Job Record Count: '.count($data).' Number of Batches: '.$totalNumberOfBatches); 64 | for ($i = 1; $i <= $totalNumberOfBatches; $i++) { 65 | $batches[] = $this->addBatch($job->id, array_splice($data, 0, $options['batchSize'])); 66 | } 67 | } else { //probably a string query so run in one batch 68 | $batches[] = $this->addBatch($job->id, $data); 69 | } 70 | } else { 71 | $this->log('error', 'Job Failed: '.json_encode($job->toArrayAll())); 72 | } 73 | 74 | $time = time(); 75 | $timeout = $time + $options['batchTimeout']; 76 | 77 | if ($options['Sforce-Enable-PKChunking']) { 78 | $batches = $this->allBatchDetails($job->id, $options['contentType']); 79 | } 80 | 81 | $batches_finished = []; 82 | 83 | while (count($batches_finished) < count($batches) && $time < $timeout) { 84 | $last_time_start = time(); 85 | foreach ($batches as &$batch) { 86 | //skip processing if batch is already done processing 87 | if (in_array($batch->id, $batches_finished)) { 88 | continue; 89 | } 90 | 91 | $batch = $this->batchDetails($job->id, $batch->id, $options['contentType']); 92 | if (in_array($batch->state, ['Completed', 'Failed', 'Not Processed', 'NotProcessed'])) { 93 | if (in_array($batch->state, ['Completed'])) { 94 | $batchResult = $this->batchResult($job->id, $batch->id, $options['isBatchedResult'], null, $options['contentType']); 95 | if (class_exists($options['batchProcessor']) && class_implements($options['batchProcessor'], '\Frankkessler\Salesforce\Interfaces\BulkBatchProcessorInterface')) { 96 | call_user_func([$options['batchProcessor'], 'process'], $batchResult); 97 | } else { 98 | $batch->records = $batchResult->records; 99 | } 100 | } 101 | $batches_finished[] = $batch->id; 102 | } 103 | } 104 | 105 | //if we aren't complete yet, look to sleep for a few seconds so we don't poll constantly 106 | if (count($batches_finished) < count($batches)) { 107 | //If the polling for all batches hasn't taken at least the amount of time set for the polling interval, wait the additional time and then continue processing. 108 | $wait_time = time() - $last_time_start; 109 | if ($wait_time < $options['pollIntervalSeconds']) { 110 | sleep($options['pollIntervalSeconds'] - $wait_time); 111 | } 112 | } 113 | $time = time(); 114 | } 115 | 116 | //only close the job is all batches finished 117 | if (count($batches_finished) == count($batches)) { 118 | $job = $this->closeJob($job->id); 119 | } 120 | 121 | $job->batches = $batches; 122 | 123 | return $job; 124 | } 125 | 126 | /** 127 | * @param string $operation 128 | * @param string $objectType 129 | * @param string $contentType 130 | * 131 | * @return BulkJobResponse 132 | */ 133 | public function createJob($operation, $objectType, $externalIdFieldName = null, $contentType = 'JSON', $concurrencyMode = 'Parallel', $options = []) 134 | { 135 | $url = '/services/async/'.SalesforceConfig::get('salesforce.api.version').'/job'; 136 | 137 | $json_array = [ 138 | 'operation' => $operation, 139 | 'object' => $objectType, 140 | 'concurrencyMode' => $concurrencyMode, 141 | ]; 142 | 143 | $headers = []; 144 | 145 | if (isset($options['Sforce-Enable-PKChunking']) && $options['Sforce-Enable-PKChunking']) { 146 | $headers['Sforce-Enable-PKChunking'] = $this->parsePkChunkingHeader($options['Sforce-Enable-PKChunking']); 147 | } 148 | 149 | //order of variables matters so this externalIdFieldName has to come before contentType 150 | if ($operation == 'upsert') { 151 | $json_array['externalIdFieldName'] = $externalIdFieldName; 152 | } 153 | 154 | $json_array['contentType'] = $contentType; 155 | 156 | $result = $this->call_api('post', $url, [ 157 | 'json' => $json_array, 158 | 'headers' => $headers, 159 | ]); 160 | 161 | if ($result && is_array($result)) { 162 | return new BulkJobResponse($result); 163 | } 164 | 165 | return new BulkJobResponse(); 166 | } 167 | 168 | public function jobDetails($jobId, $format = 'json') 169 | { 170 | $url = '/services/async/'.SalesforceConfig::get('salesforce.api.version').'/job/'.$jobId; 171 | 172 | $result = $this->call_api('get', $url, 173 | [ 174 | 'format' => $this->batchResponseFormatFromContentType($format), 175 | ]); 176 | 177 | if ($result && is_array($result)) { 178 | return new BulkJobResponse($result); 179 | } else { 180 | //throw exception 181 | } 182 | 183 | return new BulkJobResponse(); 184 | } 185 | 186 | /** 187 | * @param $jobId 188 | * 189 | * @return BulkJobResponse 190 | */ 191 | public function closeJob($jobId) 192 | { 193 | $url = '/services/async/'.SalesforceConfig::get('salesforce.api.version').'/job/'.$jobId; 194 | 195 | $json_array = [ 196 | 'state' => 'Closed', 197 | ]; 198 | 199 | $result = $this->call_api('post', $url, [ 200 | 'json' => $json_array, 201 | ]); 202 | 203 | if ($result && is_array($result)) { 204 | return new BulkJobResponse($result); 205 | } 206 | 207 | return new BulkJobResponse(); 208 | } 209 | 210 | /** 211 | * @param $jobId 212 | * @param $data 213 | * 214 | * @return BulkBatchResponse 215 | */ 216 | public function addBatch($jobId, $data, $format = 'json') 217 | { 218 | if (!$jobId) { 219 | //throw exception 220 | return new BulkBatchResponse(); 221 | } 222 | 223 | $url = '/services/async/'.SalesforceConfig::get('salesforce.api.version').'/job/'.$jobId.'/batch'; 224 | 225 | $headers = []; 226 | //json_encode any arrays to send over to bulk api 227 | if (is_array($data)) { 228 | $body = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK); 229 | $headers = [ 230 | 'Content-type' => 'application/json', 231 | ]; 232 | } else { 233 | $body = $data; 234 | } 235 | 236 | $result = $this->call_api('post', $url, [ 237 | 'body' => $body, 238 | 'headers' => $headers, 239 | 'format' => $this->batchResponseFormatFromContentType($format), 240 | ]); 241 | 242 | if ($result && is_array($result)) { 243 | return new BulkBatchResponse($result); 244 | } 245 | 246 | return new BulkBatchResponse(); 247 | } 248 | 249 | /** 250 | * @param $jobId 251 | * @param $batchId 252 | * @param $format 253 | * 254 | * @return BulkBatchResponse 255 | */ 256 | public function batchDetails($jobId, $batchId, $format = 'json') 257 | { 258 | $url = '/services/async/'.SalesforceConfig::get('salesforce.api.version').'/job/'.$jobId.'/batch/'.$batchId; 259 | 260 | $result = $this->call_api('get', $url, [ 261 | 'format' => $this->batchResponseFormatFromContentType($format), 262 | ]); 263 | 264 | if ($result && is_array($result)) { 265 | return new BulkBatchResponse($result); 266 | } else { 267 | //throw exception 268 | } 269 | 270 | return new BulkBatchResponse(); 271 | } 272 | 273 | /** 274 | * @param $jobId 275 | * @param $format 276 | * 277 | * @return BulkBatchResponse[] 278 | */ 279 | public function allBatchDetails($jobId, $format = 'json') 280 | { 281 | $batches = []; 282 | 283 | //TODO: Fix hack to give initial Salesforce batch time to split into many batches by PK 284 | sleep(10); 285 | //////////////////////////////////////////////////////////////////////////////////////// 286 | 287 | $url = '/services/async/'.SalesforceConfig::get('salesforce.api.version').'/job/'.$jobId.'/batch'; 288 | 289 | $result = $this->call_api('get', $url, [ 290 | 'format' => $this->batchResponseFormatFromContentType($format), 291 | ]); 292 | 293 | if ($result && is_array($result) && isset($result['batchInfo']) && !isset($result['batchInfo']['id'])) { 294 | foreach ($result['batchInfo'] as $batch) { 295 | $batches[] = new BulkBatchResponse($batch); 296 | } 297 | } else { 298 | //throw exception 299 | } 300 | 301 | return $batches; 302 | } 303 | 304 | /** 305 | * @param $jobId 306 | * @param $batchId 307 | * 308 | * @return BulkBatchResultResponse 309 | */ 310 | public function batchResult($jobId, $batchId, $isBatchedResult = false, $resultId = null, $format = 'json') 311 | { 312 | if (!$jobId || !$batchId) { 313 | //throw exception 314 | return new BulkBatchResultResponse(); 315 | } 316 | 317 | $url = '/services/async/'.SalesforceConfig::get('salesforce.api.version').'/job/'.$jobId.'/batch/'.$batchId.'/result'; 318 | 319 | $resultPostArray = []; 320 | 321 | //if this is a query result, the main result page will have an array of result ids to follow for hte query results 322 | if ($resultId) { 323 | $url = $url.'/'.$resultId; 324 | //results returned in contentType supplied by the creation of the job 325 | $resultPostArray['format'] = $format; 326 | //all results that have a $resultId should be returned without lowercase formatting 327 | $resultPostArray['lowerCaseHeaders'] = false; 328 | } else { 329 | //result object returned in xml if selecting csv as contentType 330 | $resultPostArray['format'] = $this->batchResponseFormatFromContentType($format); 331 | } 332 | 333 | $result = $this->call_api('get', $url, $resultPostArray); 334 | 335 | if ($result && is_array($result)) { 336 | 337 | //initialize array for records to be used later 338 | if (!isset($result['records']) || !is_array($result['records'])) { 339 | $result['records'] = []; 340 | } 341 | 342 | if (isset($result['result'])) { 343 | if (!is_array($result['result'])) { 344 | $result['result'] = [$result['result']]; 345 | } 346 | $result = array_merge($result, $result['result']); 347 | } 348 | 349 | //maximum amount of batch records allowed is 10,000 350 | for ($i = 0; $i < 10000; $i++) { 351 | //skip processing for the rest of the records if they don't exist 352 | if (!isset($result[$i])) { 353 | break; 354 | } 355 | 356 | //batched results return a list of result ids that need to be processed to get the actual data 357 | if ($isBatchedResult) { 358 | $batchResult = $this->batchResult($jobId, $batchId, false, $result[$i], $format); 359 | $result['records'] = array_merge($result['records'], $batchResult->records); 360 | } else { 361 | //fix boolean values from appearing as 362 | foreach (['success', 'created'] as $field) { 363 | if (isset($result[$i][$field])) { 364 | if ($result[$i][$field] == 'true') { 365 | $result[$i][$field] = true; 366 | } else { 367 | $result[$i][$field] = false; 368 | } 369 | } 370 | } 371 | 372 | $result['records'][$i] = $result[$i]; 373 | } 374 | 375 | unset($result[$i]); 376 | } 377 | 378 | return new BulkBatchResultResponse($result); 379 | } 380 | 381 | return new BulkBatchResultResponse($result); 382 | } 383 | 384 | /******* BINARY SPECIFIC FUNCTIONS *********/ 385 | 386 | /** 387 | * @param $operation 388 | * @param $objectType 389 | * @param BinaryBatch[] $binaryBatches 390 | * @param array $options 391 | * 392 | * @throws \Exception 393 | * 394 | * @return BulkJobResponse 395 | */ 396 | public function runBinaryUploadBatch($operation, $objectType, $binaryBatches, $options = []) 397 | { 398 | $batches = []; 399 | 400 | $defaults = [ 401 | 'batchTimeout' => 600, 402 | 'contentType' => 'ZIP_CSV', 403 | 'pollIntervalSeconds' => 5, 404 | 'isBatchedResult' => false, 405 | 'concurrencyMode' => 'Parallel', 406 | ]; 407 | 408 | $options = array_replace($defaults, $options); 409 | 410 | if ($operation == 'query') { 411 | $options['isBatchedResult'] = true; 412 | } 413 | 414 | $job = $this->createJob($operation, $objectType, null, $options['contentType'], $options['concurrencyMode']); 415 | 416 | if ($job->id) { 417 | //if data is array, we can split it into batches 418 | if (is_array($binaryBatches)) { 419 | foreach ($binaryBatches as $binaryBatch) { 420 | $batches[] = $this->addBinaryBatch($job->id, $binaryBatch); 421 | } 422 | } else { //probably a string query so run in onee batch 423 | throw(new \Exception('$binaryBatches must be an array')); 424 | } 425 | } else { 426 | $this->log('error', 'Job Failed: '.json_encode($job->toArrayAll())); 427 | } 428 | 429 | $time = time(); 430 | $timeout = $time + $options['batchTimeout']; 431 | 432 | $batches_finished = []; 433 | 434 | $resultFormat = strtolower(explode('_', $options['contentType'])[1]); 435 | while (count($batches_finished) < count($batches) && $time < $timeout) { 436 | $last_time_start = time(); 437 | foreach ($batches as &$batch) { 438 | //skip processing if batch is already done processing 439 | if (in_array($batch->id, $batches_finished)) { 440 | continue; 441 | } 442 | 443 | $batch = $this->batchDetails($job->id, $batch->id, $this->batchResponseFormatFromContentType($options['contentType'])); 444 | 445 | if (in_array($batch->state, ['Completed', 'Failed', 'Not Processed'])) { 446 | $batches_finished[] = $batch->id; 447 | $batchResult = $this->batchResult($job->id, $batch->id, $options['isBatchedResult'], null, $resultFormat); 448 | $batch->records = $batchResult->records; 449 | } 450 | } 451 | 452 | //if we aren't complete yet, look to sleep for a few seconds so we don't poll constantly 453 | if (count($batches_finished) < count($batches)) { 454 | //If the polling for all batches hasn't taken at least the amount of time set for the polling interval, wait the additional time and then continue processing. 455 | $wait_time = time() - $last_time_start; 456 | if ($wait_time < $options['pollIntervalSeconds']) { 457 | sleep($options['pollIntervalSeconds'] - $wait_time); 458 | } 459 | } 460 | $time = time(); 461 | } 462 | 463 | //only close the job is all batches finished 464 | if (count($batches_finished) == count($batches)) { 465 | $job = $this->closeJob($job->id); 466 | } 467 | 468 | $job->batches = $batches; 469 | 470 | return $job; 471 | } 472 | 473 | /** 474 | * @param $jobId 475 | * @param BinaryBatch $binaryBatch 476 | * 477 | * @return BulkBatchResponse 478 | */ 479 | public function addBinaryBatch($jobId, BinaryBatch $binaryBatch, $contentType = 'zip/csv') 480 | { 481 | if (!$jobId) { 482 | //throw exception 483 | return new BulkBatchResponse(); 484 | } 485 | 486 | $binaryBatch->prepareBatchFile(); 487 | 488 | $url = '/services/async/'.SalesforceConfig::get('salesforce.api.version').'/job/'.$jobId.'/batch'; 489 | 490 | $body = file_get_contents($binaryBatch->batchZip); 491 | $headers = [ 492 | 'Content-type' => $contentType, 493 | ]; 494 | 495 | $result = $this->call_api('post', $url, [ 496 | 'body' => $body, 497 | //'body' => fopen($binaryBatch->batchZip,'rb'), 498 | 'headers' => $headers, 499 | 'format' => $this->batchResponseFormatFromContentType($contentType), 500 | ]); 501 | 502 | if ($result && is_array($result)) { 503 | return new BulkBatchResponse($result); 504 | } 505 | 506 | return new BulkBatchResponse(); 507 | } 508 | 509 | public function batchResponseFormatFromContentType($contentType) 510 | { 511 | switch (strtoupper($contentType)) { 512 | case 'ZIP_CSV': 513 | case 'ZIP/CSV': 514 | case 'ZIP_XML': 515 | case 'ZIP/XML': 516 | case 'CSV': 517 | case 'XML': 518 | $return = 'xml'; 519 | break; 520 | default: 521 | $return = 'json'; 522 | break; 523 | } 524 | 525 | return $return; 526 | } 527 | 528 | public function parsePkChunkingHeader($pk_chunk_header) 529 | { 530 | if (is_array($pk_chunk_header)) { 531 | $header_parts = []; 532 | foreach ($pk_chunk_header as $key => $value) { 533 | $header_parts[] = $key.'='.$value; 534 | } 535 | 536 | return implode('; ', $header_parts); 537 | } elseif (in_array($pk_chunk_header, [true, 'true', 'TRUE'])) { 538 | return 'TRUE'; 539 | } 540 | 541 | return 'FALSE'; 542 | } 543 | } 544 | -------------------------------------------------------------------------------- /src/Client/BulkClient.php: -------------------------------------------------------------------------------- 1 | returnHandlers(); 18 | } 19 | 20 | parent::__construct($config); 21 | } 22 | 23 | /** 24 | * Set the middleware handlers for all requests using Oauth2. 25 | * 26 | * @return HandlerStack|null 27 | */ 28 | public function returnHandlers() 29 | { 30 | // Create a handler stack that has all of the default middlewares attached 31 | $handler = HandlerStack::create(); 32 | 33 | //Add the Authorization header to requests. 34 | $handler->push($this->mapRequest(), 'add_auth_header'); 35 | 36 | $handler->before('add_auth_header', $this->modifyRequest()); 37 | 38 | return $handler; 39 | } 40 | 41 | public function mapRequest() 42 | { 43 | return Middleware::mapRequest(function (RequestInterface $request) { 44 | if ($this->getConfig('auth') == 'bulk') { 45 | $token = $this->getAccessToken(); 46 | 47 | if ($token !== null) { 48 | $request = $request->withHeader('X-SFDC-Session', $token->getToken()); 49 | 50 | return $request; 51 | } 52 | } 53 | 54 | return $request; 55 | }); 56 | } 57 | 58 | public function modifyRequest() 59 | { 60 | return $this->retry_modify_request(function ($retries, RequestInterface $request, ResponseInterface $response = null, $error = null) { 61 | if ($retries > 0) { 62 | return false; 63 | } 64 | if ($response instanceof ResponseInterface) { 65 | if (in_array($response->getStatusCode(), [400, 401])) { 66 | return true; 67 | } 68 | } 69 | 70 | return false; 71 | }, 72 | function (RequestInterface $request, ResponseInterface $response) { 73 | if ($response instanceof ResponseInterface) { 74 | if (in_array($response->getStatusCode(), [400, 401])) { 75 | $token = $this->acquireAccessToken(); 76 | $this->setAccessToken($token, 'Bearer'); 77 | 78 | $modify['set_headers']['X-SFDC-Session'] = $token->getToken(); 79 | 80 | return Psr7\modify_request($request, $modify); 81 | } 82 | } 83 | 84 | return $request; 85 | } 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Contracts/Arrayable.php: -------------------------------------------------------------------------------- 1 | has('code')) { 24 | die; 25 | } 26 | 27 | return Authentication::processAuthenticationCode($request->input('code')); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Custom.php: -------------------------------------------------------------------------------- 1 | oauth2Client = $oauth2Client; 15 | } 16 | 17 | public function get($uri) 18 | { 19 | $url = 'https://'.SalesforceConfig::get('salesforce.api.domain').'/services/apexrest/'.$uri; 20 | 21 | return $this->oauth2Client->rawgetRequest($url); 22 | } 23 | 24 | public function post($uri, $data) 25 | { 26 | $url = 'https://'.SalesforceConfig::get('salesforce.api.domain').'/services/apexrest/'.$uri; 27 | 28 | return $this->oauth2Client->rawPostRequest($url, $data); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/DataObjects/Attachment.php: -------------------------------------------------------------------------------- 1 | localFilePath; 18 | } 19 | 20 | /** 21 | * Get the instance as an array. 22 | * 23 | * @return array 24 | */ 25 | public function toArray() 26 | { 27 | $return = get_object_public_vars($this); 28 | 29 | foreach ($return as $key => $value) { 30 | if (is_null($value)) { 31 | unset($return[$key]); 32 | } 33 | } 34 | 35 | return $return; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/DataObjects/BaseObject.php: -------------------------------------------------------------------------------- 1 | $value) { 24 | if (property_exists($this, $key) && $key != 'additional_fields') { 25 | $this->{$key} = $value; 26 | } else { 27 | $this->additional_fields[$key] = $value; 28 | } 29 | } 30 | } 31 | 32 | /** 33 | * Get Additional Fields. 34 | * 35 | * @return array 36 | */ 37 | public function getAdditionalFields() 38 | { 39 | return $this->additional_fields; 40 | } 41 | 42 | /** 43 | * Get Raw Headers from Http Response. 44 | * 45 | * @return array 46 | */ 47 | public function getRawHeaders() 48 | { 49 | return $this->raw_headers; 50 | } 51 | 52 | /** 53 | * Get Raw body from Http Response. 54 | * 55 | * @return string 56 | */ 57 | public function getRawBody() 58 | { 59 | return $this->raw_body; 60 | } 61 | 62 | /** 63 | * Get the instance as an array. 64 | * 65 | * @return array 66 | */ 67 | public function toArray() 68 | { 69 | $return = get_object_public_vars($this); 70 | 71 | return $return; 72 | } 73 | 74 | /** 75 | * Get the instance as an array. 76 | * 77 | * @return array 78 | */ 79 | public function toArrayAll() 80 | { 81 | $return = get_object_vars($this); 82 | 83 | return $return; 84 | } 85 | 86 | /** 87 | * __toString implementation for this class. 88 | * 89 | * @return string 90 | */ 91 | public function __toString() 92 | { 93 | return json_encode($this->toArray()); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/DataObjects/BinaryBatch.php: -------------------------------------------------------------------------------- 1 | attachments as $attachment) { 25 | $result[] = $attachment->toArray(); 26 | } 27 | 28 | return $result; 29 | } 30 | 31 | public function prepareBatchFile() 32 | { 33 | if ($this->batchZip && is_writable($this->batchZip)) { 34 | $zip = new ZipArchive(); 35 | if ($zip->open($this->batchZip, \ZIPARCHIVE::CREATE) === true) { 36 | if ($this->format == 'csv') { 37 | $request_array = $this->createCsvArray($this->toArray()); 38 | $request_string = str_putcsv($request_array); 39 | } else { 40 | $request_string = json_encode($this->toArray()); 41 | } 42 | $zip->addFromString('request.txt', $request_string); 43 | $zip->close(); 44 | } else { 45 | throw(new \Exception('Batch zip cannot be opened')); 46 | } 47 | } else { 48 | throw(new \Exception('Batch zip must be defined to use binary batches')); 49 | } 50 | } 51 | 52 | public function createCsvArray($input) 53 | { 54 | $i = 1; 55 | $result = []; 56 | 57 | foreach ($input as $row) { 58 | if ($i == 1) { 59 | $result[] = array_keys($row); 60 | } 61 | $result[] = array_values($row); 62 | $i++; 63 | } 64 | 65 | return $result; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/DataObjects/SalesforceError.php: -------------------------------------------------------------------------------- 1 | fields = isset($data['fields']) ? $data['fields'] : ''; 28 | $this->message = isset($data['message']) ? $data['message'] : ''; 29 | $this->errorCode = isset($data['errorCode']) ? $data['errorCode'] : ''; 30 | } 31 | 32 | public function isValid() 33 | { 34 | if ($this->message || $this->errorCode) { 35 | return true; 36 | } 37 | 38 | return false; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Facades/Salesforce.php: -------------------------------------------------------------------------------- 1 | where('user_id', $user_id); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Providers/SalesforceLaravelServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 19 | __DIR__.'/../../migrations/salesforce.php' => base_path('/database/migrations/2015_09_18_141101_create_salesforce_tokens_table.php'), 20 | ], 'migrations'); 21 | 22 | //publish config 23 | $this->publishes([ 24 | __DIR__.'/../../config/salesforce.php' => config_path('salesforce.php'), 25 | ], 'config'); 26 | 27 | //merge default config if values were removed or never published 28 | $this->mergeConfigFrom(__DIR__.'/../../config/salesforce.php', 'salesforce'); 29 | 30 | //set custom package views folder 31 | $this->loadViewsFrom(__DIR__.'/../../resources/views', 'salesforce'); 32 | 33 | //set custom routes for admin pages 34 | if (SalesforceConfig::get('salesforce.enable_oauth_routes')) { 35 | if (!$this->app->routesAreCached()) { 36 | require __DIR__.'/../../http/routes.php'; 37 | } 38 | } 39 | } 40 | 41 | /** 42 | * Register the application services. 43 | * 44 | * @return void 45 | */ 46 | public function register() 47 | { 48 | $this->app->singleton('salesforce', function ($app) { 49 | return $app->make('Frankkessler\Salesforce\Salesforce', [ 50 | 'config' => [ 51 | 'salesforce.logger' => $app['log'], 52 | ], 53 | ]); 54 | }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Query.php: -------------------------------------------------------------------------------- 1 | oauth2Client = $oauth2Client; 18 | } 19 | 20 | /** 21 | * SOQL Query. 22 | * 23 | * @param $query 24 | * 25 | * @return QueryResponse 26 | */ 27 | public function query($query) 28 | { 29 | return new QueryResponse( 30 | $this->oauth2Client->call_api('get', 'query/?q='.urlencode($query)) 31 | ); 32 | } 33 | 34 | /** 35 | * SOQL Query and follow next URL until all records are gathered. 36 | * 37 | * @param $query 38 | * 39 | * @return QueryResponse 40 | */ 41 | public function queryFollowNext($query) 42 | { 43 | return new QueryResponse( 44 | $this->_queryFollowNext('query', $query) 45 | ); 46 | } 47 | 48 | /** 49 | * SOQL Query including deleted records. 50 | * 51 | * @param $query 52 | * 53 | * @return QueryResponse 54 | */ 55 | public function queryAll($query) 56 | { 57 | return new QueryResponse( 58 | $this->oauth2Client->call_api('get', 'queryAll/?q='.urlencode($query)) 59 | ); 60 | } 61 | 62 | /** 63 | * SOQL Query including deleted records and follow next URL until all records are gathered. 64 | * 65 | * @param $query 66 | * 67 | * @return QueryResponse 68 | */ 69 | public function queryAllFollowNext($query) 70 | { 71 | return new QueryResponse( 72 | $this->_queryFollowNext('queryAll', $query) 73 | ); 74 | } 75 | 76 | /** 77 | * Search using the SOSL query language. 78 | * 79 | * @param $query 80 | * 81 | * @return SearchResponse 82 | */ 83 | public function search($query) 84 | { 85 | //TODO: put response records into records parameter 86 | return new SearchResponse( 87 | $this->oauth2Client->call_api('get', 'search/?q='.urlencode($query)) 88 | ); 89 | } 90 | 91 | protected function _queryFollowNext($query_type, $query, $url = null) 92 | { 93 | //next url has not been supplied 94 | if (is_null($url)) { 95 | $result = $this->oauth2Client->call_api('get', $query_type.'/?q='.urlencode($query)); 96 | } else { 97 | $result = $this->oauth2Client->rawGetRequest($url); 98 | } 99 | 100 | if ($result && isset($result['records']) && $result['records']) { 101 | if (isset($result['nextRecordsUrl']) && $result['nextRecordsUrl']) { 102 | $new_result = $this->_queryFollowNext($query_type, $query, $result['nextRecordsUrl']); 103 | if ($new_result && isset($new_result['records'])) { 104 | $result['records'] = array_merge($result['records'], $new_result['records']); 105 | } 106 | } 107 | } 108 | 109 | return $result; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Repositories/Eloquent/TokenEloquentRepository.php: -------------------------------------------------------------------------------- 1 | getTokenRecord($user_id); 19 | 20 | $record->access_token = $access_token; 21 | $record->save(); 22 | } 23 | 24 | public function setRefreshToken($refresh_token, $user_id = null) 25 | { 26 | $record = $this->getTokenRecord($user_id); 27 | 28 | $record->refresh_token = $refresh_token; 29 | $record->save(); 30 | } 31 | 32 | public function getTokenRecord($user_id = null) 33 | { 34 | if (is_null($user_id)) { 35 | $user_id = SalesforceConfig::get('salesforce.storage_global_user_id'); 36 | if (is_null($user_id)) { 37 | if (class_exists('\Auth') && $user = \Auth::user()) { 38 | $user_id = $user->id; 39 | } else { 40 | $user_id = 0; 41 | } 42 | } 43 | } 44 | 45 | $record = SalesforceToken::findByUserId($user_id)->first(); 46 | 47 | if (!$record) { 48 | $record = new SalesforceToken(); 49 | $record->user_id = $user_id; 50 | } 51 | 52 | return $record; 53 | } 54 | 55 | public function setTokenRecord(AccessToken $token, $user_id = null) 56 | { 57 | if (is_null($user_id)) { 58 | $user_id = SalesforceConfig::get('salesforce.storage_global_user_id'); 59 | if (is_null($user_id)) { 60 | if (class_exists('\Auth') && $user = \Auth::user()) { 61 | $user_id = $user->id; 62 | } else { 63 | $user_id = 0; 64 | } 65 | } 66 | } 67 | 68 | $record = SalesforceToken::findByUserId($user_id)->first(); 69 | 70 | if (!$record) { 71 | $record = new SalesforceToken(); 72 | $record->user_id = $user_id; 73 | } 74 | 75 | $token_data = $token->getData(); 76 | 77 | $record->access_token = $token->getToken(); 78 | $record->refresh_token = $token->getRefreshToken()->getToken(); 79 | $record->instance_base_url = $token_data['instance_url']; 80 | 81 | $record->save(); 82 | 83 | return $record; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Repositories/TokenRepository.php: -------------------------------------------------------------------------------- 1 | store = $this->setStore($config); 13 | } 14 | 15 | /** 16 | * @param array $config 17 | * 18 | * @return TokenRepositoryInterface 19 | */ 20 | public function setStore($config = []) 21 | { 22 | $store_name = SalesforceConfig::get('salesforce.storage_type'); 23 | 24 | return $this->{'create'.ucfirst($store_name).'Driver'}($config); 25 | } 26 | 27 | public function createEloquentDriver($config = []) 28 | { 29 | return new TokenEloquentRepository(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Repositories/TokenRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | records = json_decode($data['raw_body'], true); 24 | 25 | $this->totalSize = count($this->records); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Responses/SalesforceBaseResponse.php: -------------------------------------------------------------------------------- 1 | error = new SalesforceError(json_decode($data['raw_sfdc_error'], true)); 39 | } else { 40 | $this->error = new SalesforceError([]); 41 | } 42 | 43 | if (isset($data['http_status'])) { 44 | $this->http_status_code = (int) $data['http_status']; 45 | unset($data['http_status']); 46 | } 47 | parent::__construct($data); 48 | 49 | if (!$this->success && $this->http_status_code >= 200 && $this->http_status_code <= 299) { 50 | $this->success = true; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Responses/Sobject/SobjectDeleteResponse.php: -------------------------------------------------------------------------------- 1 | sobject = json_decode($data['raw_body']); 17 | 18 | parent::__construct($data); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Responses/Sobject/SobjectInsertResponse.php: -------------------------------------------------------------------------------- 1 | config_local = SalesforceConfig::get(); 43 | 44 | $this->repository = new TokenRepository(); 45 | 46 | if (isset($this->config_local['base_uri'])) { 47 | $base_uri = $this->config_local['base_uri']; 48 | } else { 49 | $base_uri = 'https://'.SalesforceConfig::get('salesforce.api.domain').SalesforceConfig::get('salesforce.api.base_uri'); 50 | } 51 | 52 | $client_config = [ 53 | 'base_uri' => $base_uri, 54 | 'auth' => 'oauth2', 55 | ]; 56 | 57 | //allow for override of default oauth2 handler 58 | if (isset($this->config_local['handler'])) { 59 | $client_config['handler'] = $this->config_local['handler']; 60 | } 61 | 62 | if (!$this->oauth2Client) { 63 | $this->oauth2Client = new Oauth2Client($client_config); 64 | } 65 | 66 | $this->setupOauthClient(); 67 | } 68 | 69 | public function setupOauthClient() 70 | { 71 | //If access_token or refresh_token are NOT supplied through constructor, pull them from the repository 72 | if (SalesforceConfig::get('salesforce.oauth.auth_type') != 'jwt_web_token' && (!SalesforceConfig::get('salesforce.oauth.access_token') || !SalesforceConfig::get('salesforce.oauth.refresh_token'))) { 73 | $token_record = $this->repository->store->getTokenRecord(); 74 | SalesforceConfig::set('salesforce.oauth.access_token', $token_record->access_token); 75 | SalesforceConfig::set('salesforce.oauth.refresh_token', $token_record->refresh_token); 76 | } 77 | 78 | $access_token = SalesforceConfig::get('salesforce.oauth.access_token'); 79 | $refresh_token = SalesforceConfig::get('salesforce.oauth.refresh_token'); 80 | 81 | //Set access token and refresh token in Guzzle oauth client 82 | $this->oauth2Client->setAccessToken($access_token, $access_token_type = 'Bearer'); 83 | 84 | if (isset($this->config_local['token_url'])) { 85 | $token_url = $this->config_local['token_url']; 86 | } else { 87 | $token_url = 'https://'.SalesforceConfig::get('salesforce.oauth.domain').SalesforceConfig::get('salesforce.oauth.token_uri'); 88 | } 89 | 90 | if (SalesforceConfig::get('salesforce.oauth.auth_type') == 'jwt_web_token') { 91 | $jwt_token_config = [ 92 | 'client_id' => SalesforceConfig::get('salesforce.oauth.consumer_token'), 93 | 'client_secret' => SalesforceConfig::get('salesforce.oauth.consumer_secret'), 94 | 'token_url' => $token_url, 95 | 'auth_location' => 'body', 96 | 'jwt_private_key' => SalesforceConfig::get('salesforce.oauth.jwt.private_key'), 97 | 'jwt_private_key_passphrase' => SalesforceConfig::get('salesforce.oauth.jwt.private_key_passphrase'), 98 | 'jwt_algorithm' => 'RS256', 99 | 'jwt_payload' => [ 100 | 'sub' => SalesforceConfig::get('salesforce.oauth.jwt.run_as_user_name'), 101 | 'aud' => 'https://'.SalesforceConfig::get('salesforce.oauth.domain'), 102 | ], 103 | ]; 104 | $grantType = new JwtBearer($jwt_token_config); 105 | $this->oauth2Client->setGrantType($grantType); 106 | } else { //web_server is default auth type 107 | $this->oauth2Client->setRefreshToken($refresh_token); 108 | 109 | $refresh_token_config = [ 110 | 'client_id' => SalesforceConfig::get('salesforce.oauth.consumer_token'), 111 | 'client_secret' => SalesforceConfig::get('salesforce.oauth.consumer_secret'), 112 | 'refresh_token' => $refresh_token, 113 | 'token_url' => $token_url, 114 | 'auth_location' => 'body', 115 | ]; 116 | $this->oauth2Client->setRefreshTokenGrantType(new RefreshToken($refresh_token_config)); 117 | } 118 | } 119 | 120 | /** 121 | * Get full sObject (DEPRECATED). 122 | * 123 | * @deprecated 124 | * @codeCoverageIgnore 125 | * 126 | * @param $id 127 | * @param $type 128 | * 129 | * @return array|mixed 130 | */ 131 | public function getObject($id, $type) 132 | { 133 | $result = $this->sobject()->get($id, $type); 134 | 135 | $array_result = array_replace($result->toArray(), json_decode(json_encode($result->sobject), true)); 136 | 137 | if ($result->error->isValid()) { 138 | $array_result['message_string'] = $result->error->message; 139 | } 140 | 141 | return $array_result; 142 | } 143 | 144 | /** 145 | * Create sObject (DEPRECATED). 146 | * 147 | * @deprecated 148 | * @codeCoverageIgnore 149 | * 150 | * @param string $type 151 | * @param array $data 152 | * 153 | * @return array|mixed 154 | */ 155 | public function createObject($type, $data) 156 | { 157 | $result = $this->sobject()->insert($type, $data); 158 | 159 | $array_result = $result->toArray(); 160 | 161 | $array_result['Id'] = $result->id; 162 | 163 | if ($result->error->isValid()) { 164 | $array_result['message_string'] = $result->error->message; 165 | } 166 | 167 | return $array_result; 168 | } 169 | 170 | /** 171 | * Update sObject (DEPRECATED). 172 | * 173 | * @deprecated 174 | * @codeCoverageIgnore 175 | * 176 | * @param string $id 177 | * @param string $type 178 | * @param array $data 179 | * 180 | * @return array|mixed 181 | */ 182 | public function updateObject($id, $type, $data) 183 | { 184 | $result = $this->sobject()->update($id, $type, $data); 185 | 186 | $array_result = $result->toArray(); 187 | 188 | if ($result->error->isValid()) { 189 | $array_result['message_string'] = $result->error->message; 190 | } 191 | 192 | return $array_result; 193 | } 194 | 195 | /** 196 | * Delete Object (DEPRECATED). 197 | * 198 | * @deprecated 199 | * @codeCoverageIgnore 200 | * 201 | * @param $id 202 | * @param $type 203 | * 204 | * @return array 205 | */ 206 | public function deleteObject($id, $type) 207 | { 208 | $result = $this->sobject()->delete($id, $type); 209 | 210 | $array_result = $result->toArray(); 211 | 212 | if ($result->error->isValid()) { 213 | $array_result['message_string'] = $result->error->message; 214 | } 215 | 216 | return $array_result; 217 | } 218 | 219 | /** 220 | * Get Object by External Id (DEPRECATED). 221 | * 222 | * @deprecated 223 | * @codeCoverageIgnore 224 | * 225 | * @param $external_field_name 226 | * @param $external_id 227 | * @param $type 228 | * 229 | * @return array 230 | */ 231 | public function externalGetObject($external_field_name, $external_id, $type) 232 | { 233 | $result = $this->sobject()->externalGet($external_field_name, $external_id, $type); 234 | 235 | $array_result = $result->toArray(); 236 | 237 | if ($result->error->isValid()) { 238 | $array_result['message_string'] = $result->error->message; 239 | } 240 | 241 | return $array_result; 242 | } 243 | 244 | /** 245 | * Upsert an object by an External Id. 246 | * 247 | * @deprecated 248 | * @codeCoverageIgnore 249 | * 250 | * @param $external_field_name 251 | * @param $external_id 252 | * @param $type 253 | * @param $data 254 | * 255 | * @return array 256 | */ 257 | public function externalUpsertObject($external_field_name, $external_id, $type, $data) 258 | { 259 | $result = $this->sobject()->externalUpsert($external_field_name, $external_id, $type, $data); 260 | 261 | $array_result = $result->toArray(); 262 | 263 | $array_result['Id'] = $result->id; 264 | 265 | if ($result->error->isValid()) { 266 | $array_result['message_string'] = $result->error->message; 267 | } 268 | 269 | return $array_result; 270 | } 271 | 272 | /** 273 | * SOQL Query (DEPRECATED). 274 | * 275 | * @deprecated 276 | * @codeCoverageIgnore 277 | * 278 | * @param $query 279 | * 280 | * @return array 281 | */ 282 | public function query_legacy($query) 283 | { 284 | return $this->query()->query($query)->toArray(); 285 | } 286 | 287 | /** 288 | * SOQL Query and follow next URL until all records are gathered (DEPRECATED). 289 | * 290 | * @deprecated 291 | * @codeCoverageIgnore 292 | * 293 | * @param $query 294 | * 295 | * @return array 296 | */ 297 | public function queryFollowNext($query) 298 | { 299 | return $this->query()->queryFollowNext($query)->toArray(); 300 | } 301 | 302 | /** 303 | * SOQL Query including deleted records (DEPRECATED). 304 | * 305 | * @deprecated 306 | * @codeCoverageIgnore 307 | * 308 | * @param $query 309 | * 310 | * @return array 311 | */ 312 | public function queryAll($query) 313 | { 314 | return $this->query()->queryAll($query)->toArray(); 315 | } 316 | 317 | /** 318 | * SOQL Query including deleted records and follow next URL until all records are gathered (DEPRECATED). 319 | * 320 | * @deprecated 321 | * @codeCoverageIgnore 322 | * 323 | * @param $query 324 | * 325 | * @return array 326 | */ 327 | public function queryAllFollowNext($query) 328 | { 329 | return $this->query()->queryAllFollowNext($query)->toArray(); 330 | } 331 | 332 | /** 333 | * SOSL Query (DEPRECATED). 334 | * 335 | * @deprecated 336 | * @codeCoverageIgnore 337 | * 338 | * @param $query 339 | * 340 | * @return array 341 | */ 342 | public function search($query) 343 | { 344 | //TODO: put response records into records parameter 345 | return $this->query()->search($query)->records; 346 | } 347 | 348 | /** 349 | * GET request for custom APEX web service endpoint (DEPRECATED. 350 | * 351 | * @deprecated 352 | * @codeCoverageIgnore 353 | * 354 | * @param $uri 355 | * 356 | * @return mixed 357 | */ 358 | public function getCustomRest($uri) 359 | { 360 | return $this->custom()->get($uri); 361 | } 362 | 363 | /** 364 | * POST request for custom APEX web service endpoint (DEPRECATED). 365 | * 366 | * @deprecated 367 | * @codeCoverageIgnore 368 | * 369 | * @param $uri 370 | * @param $data 371 | * 372 | * @return mixed 373 | */ 374 | public function postCustomRest($uri, $data) 375 | { 376 | return $this->custom()->post($uri, $data); 377 | } 378 | 379 | public function rawGetRequest($request_string) 380 | { 381 | return $this->call_api('get', $request_string); 382 | } 383 | 384 | public function rawPostRequest($request_string, $data) 385 | { 386 | return $this->call_api('post', $request_string, [ 387 | 'http_errors' => false, 388 | 'body' => json_encode($data), 389 | 'headers' => [ 390 | 'Content-type' => 'application/json', 391 | ], 392 | ]); 393 | } 394 | 395 | /** 396 | * @return Sobject 397 | */ 398 | public function sobject() 399 | { 400 | if (!$this->sobject_api) { 401 | $this->sobject_api = new Sobject($this); 402 | } 403 | 404 | return $this->sobject_api; 405 | } 406 | 407 | /** 408 | * @return Query 409 | */ 410 | public function query($legacy_query = null) 411 | { 412 | //TODO: DEPRECATE IN NEXT RELEASE 413 | if ($legacy_query) { 414 | return $this->query_legacy($legacy_query); 415 | } 416 | 417 | if (!$this->query_api) { 418 | $this->query_api = new Query($this); 419 | } 420 | 421 | return $this->query_api; 422 | } 423 | 424 | /** 425 | * @return Custom 426 | */ 427 | public function custom() 428 | { 429 | if (!$this->custom_api) { 430 | $this->custom_api = new Custom($this); 431 | } 432 | 433 | return $this->custom_api; 434 | } 435 | 436 | /** 437 | * @return Bulk 438 | */ 439 | public function bulk() 440 | { 441 | if (!$this->bulk_api) { 442 | $this->bulk_api = new Bulk($this->config_local); 443 | } 444 | 445 | return $this->bulk_api; 446 | } 447 | 448 | /** 449 | * Api Call to Salesforce. 450 | * 451 | * @param $method 452 | * @param $url 453 | * @param array $options 454 | * @param array $debug_info 455 | * 456 | * @return array 457 | */ 458 | public function call_api($method, $url, $options = [], $debug_info = []) 459 | { 460 | try { 461 | if (is_null($options)) { 462 | $options = []; 463 | } 464 | 465 | $options['http_errors'] = false; 466 | 467 | $format = 'json'; 468 | if (isset($options['format'])) { 469 | $format = strtolower($options['format']); 470 | unset($options['format']); 471 | } 472 | 473 | //required so csv return matches json return when creating new records 474 | $lowerCaseHeaders = true; 475 | if (isset($options['lowerCaseHeaders'])) { 476 | $lowerCaseHeaders = $options['lowerCaseHeaders']; 477 | unset($options['lowerCaseHeaders']); 478 | } 479 | 480 | $response = $this->oauth2Client->{$method}($url, $options); 481 | 482 | /* @var $response \GuzzleHttp\Psr7\Response */ 483 | 484 | $headers = $response->getHeaders(); 485 | 486 | $response_code = $response->getStatusCode(); 487 | 488 | $data = [ 489 | 'operation' => '', 490 | 'success' => false, 491 | 'message_string' => '', 492 | 'http_status' => $response_code, 493 | 'raw_headers' => $headers, 494 | 'raw_body' => (string) $response->getBody(), 495 | ]; 496 | 497 | if ($format == 'xml') { 498 | $xml = simplexml_load_string((string) $response->getBody(), null, LIBXML_NOCDATA); 499 | $json = json_encode($xml); 500 | $response_array = json_decode($json, true); 501 | } elseif ($format == 'csv') { 502 | $response_array = csvToArray((string) $response->getBody(), $lowerCaseHeaders); 503 | } else { 504 | $response_array = json_decode((string) $response->getBody(), true); 505 | } 506 | 507 | if ($response_code == 200) { 508 | if (is_array($response_array)) { 509 | $data = array_replace($data, $response_array); 510 | } 511 | 512 | $data['success'] = true; 513 | $data['http_status'] = 200; 514 | } elseif ($response_code == 201) { 515 | if (is_array($response_array)) { 516 | $data = array_replace($data, $response_array); 517 | } 518 | $data['operation'] = 'create'; 519 | $data['success'] = true; 520 | $data['http_status'] = 201; 521 | 522 | if (isset($data['id'])) { 523 | //make responses more uniform by setting a newly created id as an Id field like you would see from a get 524 | $data['Id'] = $data['id']; 525 | } 526 | } elseif ($response_code == 204) { 527 | if (strtolower($method) == 'delete') { 528 | $data = array_merge($data, [ 529 | 'success' => true, 530 | 'operation' => 'delete', 531 | 'http_status' => 204, 532 | ]); 533 | } else { 534 | $data = array_merge($data, [ 535 | 'success' => true, 536 | 'operation' => 'update', 537 | 'http_status' => 204, 538 | ]); 539 | } 540 | } else { 541 | if (!is_array($response_array)) { 542 | $data = array_merge($data, ['message' => $response_array]); 543 | } elseif (count($response_array) > 1) { 544 | $data = array_merge($data, $response_array); 545 | } else { 546 | $data['raw_sfdc_error'] = (string) $response->getBody(); 547 | $data = array_merge($data, current($response_array)); 548 | } 549 | 550 | if ($data && isset($data['message'])) { 551 | $data['message_string'] = $data['message']; 552 | } elseif (!$data) { 553 | $data['message_string'] = (string) $response->getBody(); 554 | } 555 | 556 | $data['http_status'] = $response_code; 557 | $data['success'] = false; 558 | 559 | $data = array_merge($debug_info, $data); 560 | 561 | $this->log('error', 'Salesforce - '.json_encode($data)); 562 | } 563 | 564 | if (isset($data) && $data) { 565 | $this->updateAccessToken($this->oauth2Client->getAccessToken()->getToken()); 566 | 567 | return $data; 568 | } 569 | } catch (Exception $e) { 570 | $data['message_string'] = $e->getMessage(); 571 | $data['file'] = $e->getFile().':'.$e->getLine(); 572 | $data['http_status'] = 500; 573 | $data['success'] = false; 574 | $data = array_merge($debug_info, $data); 575 | 576 | $this->log('error', 'Salesforce-Salesforce::call_api - '.$e->getMessage().' - '.$e->getFile().':'.$e->getLine()); 577 | 578 | return $data; 579 | } 580 | 581 | return []; 582 | } 583 | 584 | protected function log($level, $message) 585 | { 586 | if ($this->config_local['salesforce.logger'] instanceof \Psr\Log\LoggerInterface && is_callable([$this->config_local['salesforce.logger'], $level])) { 587 | return call_user_func([$this->config_local['salesforce.logger'], $level], $message); 588 | } else { 589 | return; 590 | } 591 | } 592 | 593 | protected function updateAccessToken($current_access_token) 594 | { 595 | if ($current_access_token != SalesforceConfig::get('salesforce.oauth.access_token') && SalesforceConfig::get('salesforce.oauth.auth_type') != 'jwt_web_token') { 596 | $this->repository->store->setAccessToken($current_access_token); 597 | SalesforceConfig::set('salesforce.oauth.access_token', $current_access_token); 598 | } 599 | } 600 | } 601 | -------------------------------------------------------------------------------- /src/SalesforceConfig.php: -------------------------------------------------------------------------------- 1 | $config]; 60 | 61 | return array_dot($config); 62 | } 63 | 64 | public static function reset() 65 | { 66 | self::$config = []; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Sobject.php: -------------------------------------------------------------------------------- 1 | oauth2Client = $oauth2Client; 20 | } 21 | 22 | /** 23 | * Get full sObject. 24 | * 25 | * @param $id 26 | * @param $type 27 | * 28 | * @return SobjectGetResponse 29 | */ 30 | public function get($id, $type) 31 | { 32 | return new SobjectGetResponse( 33 | $this->oauth2Client->call_api('get', 'sobjects/'.$type.'/'.$id) 34 | ); 35 | } 36 | 37 | /** 38 | * Create sObject. 39 | * 40 | * @param string $type 41 | * @param array $data 42 | * 43 | * @return SobjectInsertResponse 44 | */ 45 | public function insert($type, $data) 46 | { 47 | return new SobjectInsertResponse( 48 | $this->oauth2Client->call_api('post', 'sobjects/'.$type, [ 49 | 'http_errors' => false, 50 | 'body' => json_encode($data), 51 | 'headers' => [ 52 | 'Content-type' => 'application/json', 53 | ], 54 | ]) 55 | ); 56 | } 57 | 58 | /** 59 | * Update sObject. 60 | * 61 | * @param string $id 62 | * @param string $type 63 | * @param array $data 64 | * 65 | * @return SobjectUpdateResponse 66 | */ 67 | public function update($id, $type, $data) 68 | { 69 | if (!$id && isset($data['id'])) { 70 | $id = $data['id']; 71 | unset($data['id']); 72 | } elseif (isset($data['id'])) { 73 | unset($data['id']); 74 | } 75 | 76 | if (!$id || !$type || !$data) { 77 | return new SobjectUpdateResponse([]); 78 | } 79 | 80 | return new SobjectUpdateResponse( 81 | $this->oauth2Client->call_api('patch', 'sobjects/'.$type.'/'.$id, [ 82 | 'http_errors' => false, 83 | 'body' => json_encode($data), 84 | 'headers' => [ 85 | 'Content-type' => 'application/json', 86 | ], 87 | ]) 88 | ); 89 | } 90 | 91 | /** 92 | * @param $id 93 | * @param $type 94 | * 95 | * @return array|SobjectDeleteResponse 96 | */ 97 | public function delete($id, $type) 98 | { 99 | if (!$type || !$id) { 100 | return new SobjectDeleteResponse(); 101 | } 102 | 103 | return new SobjectDeleteResponse( 104 | $this->oauth2Client->call_api('delete', 'sobjects/'.$type.'/'.$id) 105 | ); 106 | } 107 | 108 | public function externalGet($external_field_name, $external_id, $type) 109 | { 110 | return new SobjectGetResponse( 111 | $this->oauth2Client->call_api('get', 'sobjects/'.$type.'/'.$external_field_name.'/'.$external_id) 112 | ); 113 | } 114 | 115 | public function externalUpsert($external_field_name, $external_id, $type, $data) 116 | { 117 | return new SobjectInsertResponse( 118 | $this->oauth2Client->call_api('patch', 'sobjects/'.$type.'/'.$external_field_name.'/'.$external_id, [ 119 | 'http_errors' => false, 120 | 'body' => json_encode($data), 121 | 'headers' => [ 122 | 'Content-type' => 'application/json', 123 | ], 124 | ]) 125 | ); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 | 1 && \Illuminate\Support\Str::startsWith($value, '"') && \Illuminate\Support\Str::endsWith($value, '"')) { 46 | return substr($value, 1, -1); 47 | } 48 | 49 | return $value; 50 | } 51 | } 52 | 53 | if (!function_exists('str_putcsv')) { 54 | function str_putcsv($input, $delimiter = ',', $enclosure = '"') 55 | { 56 | $fp = fopen('php://temp', 'r+b'); 57 | foreach ($input as $row) { 58 | fputcsv($fp, $row, $delimiter, $enclosure); 59 | fwrite($fp, "\r\n"); 60 | } 61 | rewind($fp); 62 | $data = stream_get_contents($fp); 63 | fclose($fp); 64 | 65 | return $data; 66 | } 67 | } 68 | 69 | if (!function_exists('csvToArray')) { 70 | function csvToArray($csv_string, $lowerCaseHeaders = false) 71 | { 72 | $csv = array_map('str_getcsv', explode("\n", $csv_string)); 73 | $header = array_shift($csv); 74 | 75 | if ($lowerCaseHeaders) { 76 | $header = array_map('strtolower', $header); 77 | } 78 | 79 | $result = []; 80 | 81 | foreach ($csv as $row) { 82 | if (count($row) == count($header)) { 83 | $result[] = array_combine($header, $row); 84 | } 85 | } 86 | 87 | return $result; 88 | } 89 | } 90 | --------------------------------------------------------------------------------