diff --git a/README.md b/README.md index 4e68531..1bffa4d 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ A simple PHP client for the Salesforce REST API Install with composer: ``` -composer config repositories.salesforce-rest-api vcs https://github.com/gmo/salesforce-rest-api composer require "gmo/salesforce-rest-api:^1.0" ``` @@ -22,11 +21,9 @@ $authentication = new Salesforce\Authentication\PasswordAuthentication( "ClientId", "ClientSecret", "Username", - "Password", - "SecurityToken", - new Http\Client() + "Password[+SecurityToken]" ); -$salesforce = new Salesforce\Client($authentication, new Http\Client(), "na5"); +$salesforce = new Salesforce\Client($authentication); try { $contactQueryResults = $salesforce->query("SELECT AccountId, LastName diff --git a/composer.json b/composer.json index 1ad6e95..146f88d 100644 --- a/composer.json +++ b/composer.json @@ -2,9 +2,10 @@ "name": "gmo/salesforce-rest-api", "license": "MIT", "require": { - "psr/log": "1.0.0", - "guzzle/guzzle": "3.9.2", - "php": ">=5.3" + "psr/log": "^1.0", + "php": "^7.1", + "guzzlehttp/guzzle": "^6.3", + "doctrine/collections": "^1.4" }, "autoload": { "psr-4": { diff --git a/src/Authentication/AuthenticationBag.php b/src/Authentication/AuthenticationBag.php new file mode 100644 index 0000000..7101ece --- /dev/null +++ b/src/Authentication/AuthenticationBag.php @@ -0,0 +1,57 @@ +token = $jsonResponse['access_token']; + $this->instanceUrl = $jsonResponse['instance_url']; + $this->tokenType = $jsonResponse['token_type']; + $this->issuedAt = (new \DateTime())->setTimestamp((int) $jsonResponse['issued_at']); + $this->signature = $jsonResponse['access_token']; + } + + public function getToken() + { + return $this->token; + } + + public function getInstanceUrl() + { + return $this->instanceUrl; + } + + public function getTokenType() + { + return $this->tokenType; + } + + public function issuedAt() + { + return $this->issuedAt; + } + + public function getSignature() + { + return $this->signature; + } +} \ No newline at end of file diff --git a/src/Authentication/AuthenticationBagInterface.php b/src/Authentication/AuthenticationBagInterface.php new file mode 100644 index 0000000..79029ac --- /dev/null +++ b/src/Authentication/AuthenticationBagInterface.php @@ -0,0 +1,19 @@ +log = $log ?: new NullLogger(); + $this->log = new NullLogger(); $this->clientId = $clientId; $this->clientSecret = $clientSecret; $this->username = $username; $this->password = $password; - $this->securityToken = $securityToken; - $this->guzzle = $guzzle; - $this->guzzle->setBaseUrl($loginApiUrl); + $this->http = $guzzle ?? new Http\Client(['base_uri' => $loginApiUrl]); } - /** - * @inheritdoc - */ - public function getAccessToken() + public function getAccessToken(): AuthenticationBagInterface { - if ($this->accessToken) { - return $this->accessToken; + if ($this->responseBag) { + return $this->responseBag; } - $postFields = array( + $postFields = [ 'grant_type' => 'password', 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'username' => $this->username, - 'password' => $this->password . $this->securityToken, - ); - $request = $this->guzzle->post('oauth2/token', null, $postFields); - $request->setAuth('user', 'pass'); - $response = $request->send(); - $responseBody = $response->getBody(); + 'password' => $this->password, + ]; + $response = $this->http->post(self::LOGIN_URL, ['form_params' => $postFields]); + $responseBody = $response->getBody()->getContents(); $jsonResponse = json_decode($responseBody, true); if ($response->getStatusCode() !== 200) { @@ -84,14 +85,14 @@ public function getAccessToken() throw new Exception\SalesforceAuthentication($message); } - $this->accessToken = $jsonResponse['access_token']; + $this->responseBag = new AuthenticationBag($jsonResponse); - return $this->accessToken; + return $this->responseBag; } public function invalidateAccessToken() { - $this->accessToken = null; + $this->responseBag = null; } /** diff --git a/src/Client.php b/src/Client.php index 4bd824a..ce7cdbe 100644 --- a/src/Client.php +++ b/src/Client.php @@ -1,48 +1,58 @@ apiBaseUrl = str_replace(array('{region}', '{version}'), array($apiRegion, $apiVersion), - static::SALESFORCE_API_URL_PATTERN); - $this->log = $log ?: new NullLogger(); + $this->log = new NullLogger(); + $this->apiVersion = $apiVersion; $this->authentication = $authentication; - $this->guzzle = $guzzle; - $this->guzzle->setBaseUrl($this->apiBaseUrl); + $this->http = $guzzle?? new Http\Client(); } /** @@ -52,9 +62,9 @@ public function __construct( * @return QueryIterator * @throws Exception\SalesforceNoResults */ - public function queryAll($queryToRun, $parameters = array()) + public function queryAll(string $queryToRun, array $parameters = []): QueryIterator { - $apiPath = $this->buildApiPathForQuery('queryAll', $queryToRun, $parameters); + $apiPath = $this->buildApiPathForQuery(self::SALESFORCE_QUERYALL, $queryToRun, $parameters); $queryResults = $this->callQueryApiAndGetQueryResults($apiPath); return new QueryIterator($this, $queryResults); @@ -66,7 +76,7 @@ public function queryAll($queryToRun, $parameters = array()) * @param array $parameters * @return string */ - protected function buildApiPathForQuery($queryMethod, $queryToRun, $parameters = array()) + protected function buildApiPathForQuery(string $queryMethod, string $queryToRun, array $parameters = []): string { if (!empty($parameters)) { $queryToRun = $this->bindParameters($queryToRun, $parameters); @@ -74,7 +84,7 @@ protected function buildApiPathForQuery($queryMethod, $queryToRun, $parameters = $queryToRun = urlencode($queryToRun); - return "{$queryMethod}/?q={$queryToRun}"; + return '/' . $queryMethod . '/?q=' . $queryToRun; } /** @@ -82,7 +92,7 @@ protected function buildApiPathForQuery($queryMethod, $queryToRun, $parameters = * @param array $parameters * @return string */ - protected function bindParameters($queryString, $parameters) + protected function bindParameters(string $queryString, array $parameters): string { $paramKeys = array_keys($parameters); $isNumericIndexes = array_reduce(array_map('is_int', $paramKeys), function ($carry, $item) { @@ -107,12 +117,12 @@ protected function bindParameters($queryString, $parameters) return str_replace($searchArray, $replaceArray, $queryString); } - protected function addQuotesToStringReplacements($replacements) + protected function addQuotesToStringReplacements(array $replacements): array { foreach ($replacements as $key => $val) { if (is_string($val) && !$this->isSalesforceDateFormat($val)) { - $val = str_replace("'", "\'", $val); - $replacements[$key] = "'{$val}'"; + $val = str_replace('\'', '\\\'', $val); + $replacements[$key] = '\'' . $val . '\''; } } @@ -124,16 +134,13 @@ protected function isSalesforceDateFormat($string) return preg_match('/\d+[-]\d+[-]\d+[T]\d+[:]\d+[:]\d+[Z]/', $string) === 1; } - protected function replaceBooleansWithStringLiterals($replacements) + protected function replaceBooleansWithStringLiterals(array $replacements): array { return array_map(function ($val) { if (!is_bool($val)) { return $val; } - - $retval = $val ? 'true' : 'false'; - - return $retval; + return $val ? 'true' : 'false'; }, $replacements); } @@ -145,7 +152,7 @@ protected function replaceBooleansWithStringLiterals($replacements) */ protected function callQueryApiAndGetQueryResults($apiPath) { - $response = $this->get($apiPath); + $response = $this->get($this->getUrl($apiPath)); $jsonResponse = json_decode($response, true); if (!isset($jsonResponse['totalSize']) || empty($jsonResponse['totalSize'])) { @@ -162,40 +169,46 @@ protected function callQueryApiAndGetQueryResults($apiPath) ); } - protected function get($path, $headers = array(), $body = null, $options = array()) + protected function get(string $url = '') { - return $this->requestWithAutomaticReauthorize('GET', $path, $headers, $body, $options); + return $this->requestWithAutomaticReauthorize('GET', $url); } protected function requestWithAutomaticReauthorize( - $type, - $path, - $headers = array(), - $body = null, - $options = array() + string $type, + string $path, + array $headers = [], + $body = null ) { try { - return $this->request($type, $path, $headers, $body, $options); + return $this->request($type, $path, $headers, $body); } catch (Exception\SessionExpired $e) { $this->authentication->invalidateAccessToken(); - $this->setAccessTokenInGuzzleFromAuthentication(); + $this->populateToken(); - return $this->request($type, $path, $headers, $body, $options); + return $this->request($type, $path, $headers, $body); } } - protected function request($type, $path, $headers = array(), $body = null, $options = array()) + protected function request(string $type, string $path, array $headers = [], $body = null) { - $this->initializeGuzzle(); - $request = $this->guzzle->createRequest($type, $path, $headers, $body, $options); - try { - $response = $request->send(); - $responseBody = $response->getBody(); + $this->initialize(); + + $options = [ + 'headers' => array_merge($headers, $this->httpHeader), + 'form_params' => $body + ]; + if (isset($options['headers']['Content-Type']) && $options['headers']['Content-Type'] === 'application/json') { + $options['json'] = $options['form_params']; + unset($options['form_params']); + } - } catch (ClientErrorResponseException $e) { + try { + $request = $this->getHttp()->request($type, $path, $options); + $responseBody = $request->getBody()->getContents(); + } catch (BadResponseException $e) { $response = $e->getResponse(); - $responseBody = $response->getBody(); - $message = $responseBody; + $message = $responseBody = $response->getBody()->getContents(); $errorCode = $response->getStatusCode(); $jsonResponse = json_decode($responseBody, true); @@ -203,7 +216,7 @@ protected function request($type, $path, $headers = array(), $body = null, $opti $message = $jsonResponse[0]['message']; } - $fields = array(); + $fields = []; if (isset($jsonResponse[0]) && isset($jsonResponse[0]['fields'])) { $fields = $jsonResponse[0]['fields']; } @@ -221,19 +234,19 @@ protected function request($type, $path, $headers = array(), $body = null, $opti /** * Lazy loads the access token by running authentication and setting the access token into the $this->guzzle headers */ - protected function initializeGuzzle() + protected function initialize() { - if ($this->guzzle->getDefaultOption('headers/Authorization')) { + if ($this->httpHeader['Authorization']) { return; } - $this->setAccessTokenInGuzzleFromAuthentication(); + $this->populateToken(); } - protected function setAccessTokenInGuzzleFromAuthentication() + protected function populateToken() { $accessToken = $this->authentication->getAccessToken(); - $this->guzzle->setDefaultOption('headers/Authorization', "Bearer {$accessToken}"); + $this->httpHeader['Authorization'] = $accessToken->getTokenType() . ' ' . $accessToken->getToken(); } /** @@ -275,32 +288,12 @@ protected function getExceptionForSalesforceError($message, $code, $fields) */ public function getNextQueryResults(QueryResults $queryResults) { - $basePath = $this->getPathFromUrl($this->apiBaseUrl); + $basePath = parse_url($this->getApiBaseUrl())['path']; $nextRecordsRelativePath = str_replace($basePath, '', $queryResults->getNextQuery()); return $this->callQueryApiAndGetQueryResults($nextRecordsRelativePath); } - protected function getPathFromUrl($url) - { - $parts = parse_url($url); - - return $parts['path']; - } - - /** - * Get a record Id via a call to the Query API - * @param $queryString - * @return string The Id field of the first result of the query - * @throws Exception\SalesforceNoResults - */ - public function queryForId($queryString) - { - $jsonResponse = $this->query($queryString); - - return $jsonResponse['records'][0]['Id']; - } - /** * Makes a call to the Query API * @param string $queryToRun @@ -308,7 +301,7 @@ public function queryForId($queryString) * @return QueryIterator * @throws Exception\SalesforceNoResults */ - public function query($queryToRun, $parameters = array()) + public function query($queryToRun, $parameters = []) { $apiPath = $this->buildApiPathForQuery('query', $queryToRun, $parameters); $queryResults = $this->callQueryApiAndGetQueryResults($apiPath); @@ -316,71 +309,6 @@ public function query($queryToRun, $parameters = array()) return new QueryIterator($this, $queryResults); } - /** - * Get an Account by Account Id - * @param string $accountId - * @param string[]|null $fields The Account fields to return. Default Name & BillingCountry - * @return mixed The API output, converted from JSON to an associative array - * @throws Exception\SalesforceNoResults - */ - public function getAccount($accountId, $fields = null) - { - $accountId = urlencode($accountId); - $defaultFields = array('Name', 'BillingCountry'); - if (empty($fields)) { - $fields = $defaultFields; - } - $fields = implode(',', $fields); - $response = $this->get("sobjects/Account/{$accountId}?fields={$fields}"); - $jsonResponse = json_decode($response, true); - - if (!isset($jsonResponse['attributes']) || empty($jsonResponse['attributes'])) { - $message = 'No results found'; - $this->log->info($message, array('response' => $response)); - throw new Exception\SalesforceNoResults($message); - } - - return $jsonResponse; - } - - /** - * Get a Contact by Account Id - * @param string $accountId - * @param string[]|null $fields The Contact fields to return. Default FirstName, LastName and MailingCountry - * @return mixed The API output, converted from JSON to an associative array - * @throws Exception\SalesforceNoResults - */ - public function getContact($accountId, $fields = null) - { - $accountId = urlencode($accountId); - $defaultFields = array('FirstName', 'LastName', 'MailingCountry'); - if (empty($fields)) { - $fields = $defaultFields; - } - $fields = implode(',', $fields); - $response = $this->get("sobjects/Contact/{$accountId}?fields={$fields}"); - $jsonResponse = json_decode($response, true); - - if (!isset($jsonResponse['attributes']) || empty($jsonResponse['attributes'])) { - $message = 'No results found'; - $this->log->info($message, array('response' => $response)); - throw new Exception\SalesforceNoResults($message); - } - - return $jsonResponse; - } - - /** - * Creates a new Account using the provided field values - * @param string[] $fields The field values to set on the new Account - * @return string The Id of the newly created Account - * @throws Exception\Salesforce - */ - public function newAccount($fields) - { - return $this->newSalesforceObject("Account", $fields); - } - /** * Creates a new Salesforce Object using the provided field values * @param string $object The name of the salesforce object. i.e. Account or Contact @@ -388,18 +316,13 @@ public function newAccount($fields) * @return string The Id of the newly created Salesforce Object * @throws Exception\Salesforce */ - public function newSalesforceObject($object, $fields) + public function create(string $object, array $fields) { - $this->log->info('Creating Salesforce object', array( - 'object' => $object, - 'fields' => $fields, - )); - $fields = json_encode($fields); - $headers = array( - 'Content-Type' => 'application/json' - ); + $this->log->info('Creating Salesforce object', ['object' => $object, 'fields' => $fields]); + $headers = ['Content-Type' => 'application/json']; - $response = $this->post("sobjects/{$object}/", $headers, $fields); + $url = sprintf(self::SALESFORCE_POST_PATTERN, $object); + $response = $this->post($this->getUrl($url), $headers, $fields); $jsonResponse = json_decode($response, true); if (!isset($jsonResponse['id']) || empty($jsonResponse['id'])) { @@ -411,31 +334,9 @@ public function newSalesforceObject($object, $fields) return $jsonResponse['id']; } - protected function post($path, $headers = array(), $body = null, $options = array()) - { - return $this->requestWithAutomaticReauthorize('POST', $path, $headers, $body, $options); - } - - /** - * Creates a new Contact using the provided field values - * @param string[] $fields The field values to set on the new Contact - * @return string The Id of the newly created Contact - * @throws Exception\Salesforce - */ - public function newContact($fields) - { - return $this->newSalesforceObject("Contact", $fields); - } - - /** - * Updates an Account using the provided field values - * @param string $id The Account Id of the Account to update - * @param string[] $fields The fields to update - * @return bool - */ - public function updateAccount($id, $fields) + protected function post(string $path, array $headers = [], $body = null) { - return $this->updateSalesforceObject("Account", $id, $fields); + return $this->requestWithAutomaticReauthorize('POST', $path, $headers, $body); } /** @@ -445,46 +346,33 @@ public function updateAccount($id, $fields) * @param string[] $fields The fields to update * @return bool */ - public function updateSalesforceObject($object, $id, $fields) + public function update(string $object, string $id, array $fields) { - $this->log->info('Updating Salesforce object', array( - 'id' => $id, - 'object' => $object, - )); + $this->log->info('Updating Salesforce object', ['id' => $id, 'object' => $object]); $id = urlencode($id); - $fields = json_encode($fields); - $headers = array( - 'Content-Type' => 'application/json' - ); + $headers = ['Content-Type' => 'application/json']; - $this->patch("sobjects/{$object}/{$id}", $headers, $fields); + $url = sprintf(self::SALESFORCE_PATCH_PATTERN, $object, $id); + $this->patch($this->getUrl($url), $headers, $fields); return true; } - protected function patch($path, $headers = array(), $body = null, $options = array()) + protected function patch(string $url, array $headers = [], $body = null) { - return $this->requestWithAutomaticReauthorize('PATCH', $path, $headers, $body, $options); + return $this->requestWithAutomaticReauthorize('PATCH', $url, $headers, $body); } - /** - * Updates an Contact using the provided field values - * @param string $id The Contact Id of the Contact to update - * @param string[] $fields The fields to update - * @return bool - */ - public function updateContact($id, $fields) + public function getSobjects() { - return $this->updateSalesforceObject("Contact", $id, $fields); - } - /** - * Gets the valid fields for Accounts via the describe API - * @return mixed The API output, converted from JSON to an associative array - */ - public function getAccountFields() - { - return $this->getFields('Account'); + $url = sprintf(self::SALESFORCE_SOBJECTS); + $jsonResponse = json_decode($this->get($this->getUrl($url)), true); + $fields = new ArrayCollection(); + foreach ($jsonResponse['sobjects'] as $row) { + $fields->add(new Sobject($row)); + } + return $fields; } /** @@ -492,27 +380,17 @@ public function getAccountFields() * @param string $object The name of the salesforce object. i.e. Account or Contact * @return mixed The API output, converted from JSON to an associative array */ - public function getFields($object) + public function getFields(string $object): ArrayCollection { - $response = $this->get("sobjects/{$object}/describe"); - $jsonResponse = json_decode($response, true); - $fields = array(); + $url = sprintf(self::SALESFORCE_DESCRIBE_PATTERN, $object); + $jsonResponse = json_decode($this->get($this->getUrl($url)), true); + $fields = new ArrayCollection(); foreach ($jsonResponse['fields'] as $row) { - $fields[] = array('label' => $row['label'], 'name' => $row['name']); + $fields->add(new Field($row)); } - return $fields; } - /** - * Gets the valid fields for Contacts via the describe API - * @return mixed The API output, converted from JSON to an associative array - */ - public function getContactFields() - { - return $this->getFields('Contact'); - } - /** * @inheritdoc */ @@ -520,4 +398,38 @@ public function setLogger(LoggerInterface $logger) { $this->log = $logger; } + + protected function getApiBaseUrl(): string + { + return $this->authentication->getAccessToken()->getInstanceUrl() . + self::SALESFORCE_API_URL . + $this->getApiVersion(); + } + + protected function getUrl(string $url): string + { + return $this->getApiBaseUrl() . $url; + } + + protected function getHttp() : Http\Client + { + if ('last' === $this->apiVersion) { + $this->getApiVersion(); + $this->http->__construct(['base_uri' => $this->getApiBaseUrl()]); + } + return $this->http; + } + + protected function getApiVersion(): string + { + if ('last' !== $this->apiVersion) { + return $this->apiVersion; + } + $this->http->__construct([ + 'base_uri' => $this->authentication->getAccessToken()->getInstanceUrl() . self::SALESFORCE_API_URL + ]); + $versions = json_decode($this->http->get('')->getBody()->getContents(), true); + $this->apiVersion = 'v' . end($versions)['version']; + return $this->apiVersion; + } } diff --git a/src/Sobject/Field.php b/src/Sobject/Field.php new file mode 100644 index 0000000..ffe7d82 --- /dev/null +++ b/src/Sobject/Field.php @@ -0,0 +1,37 @@ +name = $jsonField['name']; + $this->label = $jsonField['label']; + } + + public function getName(): string + { + return $this->name; + } + + public function getLabel(): string + { + return $this->label; + } + + public function jsonSerialize() + { + return [ + 'name' => $this->name, + 'label' => $this->label + ]; + } +} \ No newline at end of file diff --git a/src/Sobject/FieldInterface.php b/src/Sobject/FieldInterface.php new file mode 100644 index 0000000..57bf31b --- /dev/null +++ b/src/Sobject/FieldInterface.php @@ -0,0 +1,12 @@ +name = $jsonField['name']; + $this->label = $jsonField['label']; + } + + public function getName(): string + { + return $this->name; + } + + public function getLabel(): string + { + return $this->label; + } + + public function jsonSerialize() + { + return [ + 'name' => $this->name, + 'label' => $this->label + ]; + } + + public function __toString() + { + return $this->getName(); + } +} \ No newline at end of file diff --git a/src/Sobject/SobjectInterface.php b/src/Sobject/SobjectInterface.php new file mode 100644 index 0000000..57bf31b --- /dev/null +++ b/src/Sobject/SobjectInterface.php @@ -0,0 +1,12 @@ +