From 06092de8f94a6f738a4e9f23f03a7cda992b379d Mon Sep 17 00:00:00 2001 From: codemile Date: Wed, 27 Nov 2019 06:18:24 -0500 Subject: [PATCH 01/21] API fixes for CakePHP 3.8 --- src/Database/Driver/Salesforce.php | 26 ++++++++++--------- src/Database/Driver/SalesforceDriverTrait.php | 16 +++++++----- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/Database/Driver/Salesforce.php b/src/Database/Driver/Salesforce.php index 0bf630f..dae8265 100644 --- a/src/Database/Driver/Salesforce.php +++ b/src/Database/Driver/Salesforce.php @@ -46,11 +46,12 @@ class Salesforce extends Driver 'init' => [], ]; - /** - * Establishes a connection to the database server - * - * @return bool true on success - */ + /** + * Establishes a connection to the database server + * + * @return bool true on success + * @throws \ErrorException + */ public function connect() { if ($this->_connection) { @@ -58,7 +59,7 @@ public function connect() } $config = $this->_config; - $this->_connect($config); + $this->_connect('', $config); if (!empty($config['init'])) { $connection = $this->connection(); @@ -79,12 +80,13 @@ public function enabled() return true; //Dont know if I need this? } - /** - * Prepares a sql statement to be executed - * - * @param string|\Cake\Database\Query $query The query to prepare. - * @return \Cake\Database\StatementInterface - */ + /** + * Prepares a sql statement to be executed + * + * @param string|\Cake\Database\Query $query The query to prepare. + * @return \Cake\Database\StatementInterface + * @throws \ErrorException + */ public function prepare($query) { $this->connect(); diff --git a/src/Database/Driver/SalesforceDriverTrait.php b/src/Database/Driver/SalesforceDriverTrait.php index 1daa3a3..1e1de03 100644 --- a/src/Database/Driver/SalesforceDriverTrait.php +++ b/src/Database/Driver/SalesforceDriverTrait.php @@ -12,13 +12,15 @@ trait SalesforceDriverTrait protected $_connection; public $config; - /** - * Establishes a connection to the salesforce server - * - * @param array $config configuration to be used for creating connection - * @return bool true on success - */ - protected function _connect(array $config) + /** + * Establishes a connection to the salesforce server + * + * @param string $dsn A Driver-specific PDO-DSN + * @param array $config configuration to be used for creating connection + * @return bool true on success + * @throws \ErrorException + */ + protected function _connect($dns, array $config) { $this->config = $config; From 574251819c1f94be6de9f1005a2dc4734625b1e2 Mon Sep 17 00:00:00 2001 From: codemile Date: Thu, 19 Dec 2019 08:06:54 -0500 Subject: [PATCH 02/21] adds support for setting nulls when updating --- src/Database/Statement/SalesforceStatement.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Database/Statement/SalesforceStatement.php b/src/Database/Statement/SalesforceStatement.php index 5eb6558..4917255 100644 --- a/src/Database/Statement/SalesforceStatement.php +++ b/src/Database/Statement/SalesforceStatement.php @@ -108,12 +108,22 @@ protected function _buildObjectFromUpdate($sql, $bindings) preg_match('/UPDATE .* SET (.*) WHERE (.*)/', $sql, $parts); $cleanedSQL = explode(' , ', $parts[1]); $cleanedSQL[] = $parts[2]; - $newSQL = []; + $newSQL = ['fieldsToNull'=>[]]; foreach ($cleanedSQL as $row) { - $string = explode(' = ', $row); - $newSQL[trim($string[0])] = $this->_replacement($bindings[trim($string[1])]); + list($fieldName, $value) = explode(' = ', $row); + $fieldName = trim($fieldName); + $value = $this->_replacement($bindings[trim($value)]); + if ($value === '') { + $newSQL['fieldsToNull'][] = $fieldName; + } else { + $newSQL[$fieldName] = $value; + } } + if(count($newSQL['fieldsToNull']) === 0) { + unset($newSQL['fieldsToNull']); + } + //return as object return (object)$newSQL; } From 73ec1db76fca0f7e78a7a4190862a03ece64effe Mon Sep 17 00:00:00 2001 From: codemile Date: Thu, 19 Dec 2019 21:00:35 -0500 Subject: [PATCH 03/21] adds support deleting single records --- .../Statement/SalesforceStatement.php | 431 +++++++++--------- 1 file changed, 218 insertions(+), 213 deletions(-) diff --git a/src/Database/Statement/SalesforceStatement.php b/src/Database/Statement/SalesforceStatement.php index 4917255..3214cae 100644 --- a/src/Database/Statement/SalesforceStatement.php +++ b/src/Database/Statement/SalesforceStatement.php @@ -16,244 +16,249 @@ use AssignmentRuleHeader; use Cake\Database\Statement\StatementDecorator; -use Cake\Database\ValueBinder; + /** * Statement class meant to be used by a Mysql PDO driver * * @internal */ -class SalesforceStatement extends StatementDecorator -{ - - protected $last_rows_affected = 0; - protected $last_result; //pretty sure this is awful! - protected $last_row_returned = 0; - private $_last_insert_id = []; - - /** - * {@inheritDoc} - * - */ - public function execute($params = null) - { - $sql = $this->_statement->sql(); - $bindings = $this->_statement->valueBinder()->bindings(); +class SalesforceStatement extends StatementDecorator { - //intercept Update here - if ($this->_statement->type() == 'update') { - $results = $this->_driver->client->update([$this->_buildObjectFromUpdate($sql, $bindings)], $this->_statement->repository()->name); - if (is_object($results)) { - trigger_error('Unexpected object results', E_USER_ERROR); - } - $result = new \stdClass(); - if ($results[0]->success) { - $result->size = 1; - } else { - $result->size = 0; - $this->_driver->errors = $results[0]->errors; - } - } else if ($this->_statement->type() == 'insert') { - $object = $this->_buildObjectFromInsert($sql, $bindings); - if (empty($object->OwnerId)) { - $header = new AssignmentRuleHeader(null, true); // run the default lead assignment rule - $this->_driver->client->setAssignmentRuleHeader($header); - } - $results = $this->_driver->client->create([$object], $this->_statement->repository()->name); - if (is_object($results)) { - trigger_error('Unexpected object results', E_USER_ERROR); - } - $result = new \stdClass(); - if ($results[0]->success) { - $result->size = 1; - $this->_last_insert_id[$this->_statement->repository()->name] = $results[0]->id; - } else { - $result->size = 0; - $this->_driver->errors = $results[0]->errors; - } - } else { - $result = $this->_driver->client->query($this->_interpolate($sql, $bindings)); - } + protected $last_rows_affected = 0; + protected $last_result; //pretty sure this is awful! + protected $last_row_returned = 0; + private $_last_insert_id = []; - $this->last_rows_affected = $result->size; - $this->last_result = $result; - return $result; - } + /** + * {@inheritDoc} + * + */ + public function execute($params = null) { + $sql = $this->_statement->sql(); + $bindings = $this->_statement->valueBinder()->bindings(); - /** - * Helper function used to replace query placeholders by the real - * params used to execute the query. - * - * @param string $sql The sql query - * @param array $bindings List of placeholder replacement values - * @return string - */ - protected function _interpolate($sql, $bindings) - { - foreach ($bindings as $binding) { - $sql = preg_replace('/:'.$binding['placeholder'].'\b/i', $this->_replacement($binding, true), $sql); - } + //intercept Update here + if ($this->_statement->type() == 'update') { + $results = $this->_driver->client->update([$this->_buildObjectFromUpdate($sql, $bindings)], $this->_statement->repository()->name); + if (is_object($results)) { + trigger_error('Unexpected object results', E_USER_ERROR); + } + $result = new \stdClass(); + if ($results[0]->success) { + $result->size = 1; + } else { + $result->size = 0; + $this->_driver->errors = $results[0]->errors; + } + } else if ($this->_statement->type() == 'insert') { + $object = $this->_buildObjectFromInsert($sql, $bindings); + if (empty($object->OwnerId)) { + $header = new AssignmentRuleHeader(null, true); // run the default lead assignment rule + $this->_driver->client->setAssignmentRuleHeader($header); + } + $results = $this->_driver->client->create([$object], $this->_statement->repository()->name); + if (is_object($results)) { + trigger_error('Unexpected object results', E_USER_ERROR); + } + $result = new \stdClass(); + if ($results[0]->success) { + $result->size = 1; + $this->_last_insert_id[$this->_statement->repository()->name] = $results[0]->id; + } else { + $result->size = 0; + $this->_driver->errors = $results[0]->errors; + } + } else if ($this->_statement->type() == 'delete') { + if (count($bindings) == 1 && preg_match('/DELETE FROM .* WHERE Id = :c0/', $sql)) { + $id = $bindings[':c0']['value']; + $results = $this->_driver->client->delete([$id]); + if (is_object($results)) { + trigger_error('Unexpected object results', E_USER_ERROR); + } + $result = new \stdClass(); + if ($results[0]->success) { + $result->size = 1; + } else { + $result->size = 0; + $this->_driver->errors = $results[0]->errors; + } + } else { + throw new \Exception('Unsupported delete query. Only where ID clauses are supported'); + } + } else { + $result = $this->_driver->client->query($this->_interpolate($sql, $bindings)); + } - return $sql; - } + $this->last_rows_affected = $result->size; + $this->last_result = $result; + return $result; + } - /** - * Helper function used to build an sObject from an update query. - * - * @param string $sql The sql query - * @param array $bindings List of placeholder replacement values - * @return mixed - */ - protected function _buildObjectFromUpdate($sql, $bindings) - { - preg_match('/UPDATE .* SET (.*) WHERE (.*)/', $sql, $parts); - $cleanedSQL = explode(' , ', $parts[1]); - $cleanedSQL[] = $parts[2]; - $newSQL = ['fieldsToNull'=>[]]; - foreach ($cleanedSQL as $row) { - list($fieldName, $value) = explode(' = ', $row); - $fieldName = trim($fieldName); - $value = $this->_replacement($bindings[trim($value)]); - if ($value === '') { - $newSQL['fieldsToNull'][] = $fieldName; + /** + * Helper function used to build an sObject from an update query. + * + * @param string $sql The sql query + * @param array $bindings List of placeholder replacement values + * @return mixed + */ + protected function _buildObjectFromUpdate($sql, $bindings) { + preg_match('/UPDATE .* SET (.*) WHERE (.*)/', $sql, $parts); + $cleanedSQL = explode(' , ', $parts[1]); + $cleanedSQL[] = $parts[2]; + $newSQL = ['fieldsToNull' => []]; + foreach ($cleanedSQL as $row) { + [$fieldName, $value] = explode(' = ', $row); + $fieldName = trim($fieldName); + $value = $this->_replacement($bindings[trim($value)]); + if ($value === '') { + $newSQL['fieldsToNull'][] = $fieldName; } else { $newSQL[$fieldName] = $value; } - } + } - if(count($newSQL['fieldsToNull']) === 0) { - unset($newSQL['fieldsToNull']); + if (count($newSQL['fieldsToNull']) === 0) { + unset($newSQL['fieldsToNull']); } - //return as object - return (object)$newSQL; - } + //return as object + return (object)$newSQL; + } - /** - * Helper function used to build an sObject from an insert query. - * - * @param string $sql The sql query - * @param array $bindings List of placeholder replacement values - * @return mixed - */ - protected function _buildObjectFromInsert($sql, $bindings) - { - preg_match('/\((.*)\) VALUES \((.*)\)/', $sql, $blobs); - $fields = explode(', ', $blobs[1]); - $placeholders = explode(', ', $blobs[2]); - $newSQL = []; - foreach ($fields as $key => $field) { - $newSQL[$field] = $this->_replacement($bindings[$placeholders[$key]]); - } + protected function _replacement($binding, $quote = false) { + switch ($binding['type']) { + case 'integer': + case 'boolean': + return (int)$binding['value']; + case 'float': + return (float)$binding['value']; + case 'datetime': + case 'date': + $ret = (string)$binding['value']; + break; + case 'string': + $ret = trim($binding['value']); + break; + default: + $ret = addslashes(trim($binding['value'])); + break; + } + if ($quote) { + return "'$ret'"; + } + return $ret; + } - //remove empty / null values - $newSQL = array_filter($newSQL, 'strlen'); + /** + * Helper function used to build an sObject from an insert query. + * + * @param string $sql The sql query + * @param array $bindings List of placeholder replacement values + * @return mixed + */ + protected function _buildObjectFromInsert($sql, $bindings) { + preg_match('/\((.*)\) VALUES \((.*)\)/', $sql, $blobs); + $fields = explode(', ', $blobs[1]); + $placeholders = explode(', ', $blobs[2]); + $newSQL = []; + foreach ($fields as $key => $field) { + $newSQL[$field] = $this->_replacement($bindings[$placeholders[$key]]); + } - //return as object - return (object)$newSQL; - } + //remove empty / null values + $newSQL = array_filter($newSQL, 'strlen'); - protected function _replacement($binding, $quote = false) - { - switch ($binding['type']) { - case 'integer': - case 'boolean': - return (int)$binding['value']; - case 'float': - return (float)$binding['value']; - case 'datetime': - case 'date': - $ret = (string)$binding['value']; - break; - case 'string': - $ret = trim($binding['value']); - break; - default: - $ret = addslashes(trim($binding['value'])); - break; - } - if ($quote) { - return "'$ret'"; - } - return $ret; - } + //return as object + return (object)$newSQL; + } - public function rowCount() - { - return $this->last_rows_affected; - } + /** + * Helper function used to replace query placeholders by the real + * params used to execute the query. + * + * @param string $sql The sql query + * @param array $bindings List of placeholder replacement values + * @return string + */ + protected function _interpolate($sql, $bindings) { + foreach ($bindings as $binding) { + $sql = preg_replace('/:' . $binding['placeholder'] . '\b/i', $this->_replacement($binding, true), $sql); + } + + return $sql; + } + + public function rowCount() { + return $this->last_rows_affected; + } - /** - * Returns the next row for the result set after executing this statement. - * Rows can be fetched to contain columns as names or positions. If no - * rows are left in result set, this method will return false - * - * ### Example: - * - * ``` - * $statement = $connection->prepare('SELECT id, title from articles'); - * $statement->execute(); - * print_r($statement->fetch('assoc')); // will show ['id' => 1, 'title' => 'a title'] - * ``` - * - * @param string $type 'num' for positional columns, 'assoc' for named columns - * @return mixed Result array containing columns and values or false if no results - * are left - */ - public function fetch($type = 'num') - { - if ($type === 'num') { - $result = (array)$this->last_result->records[$this->last_row_returned]; - } - if ($type === 'assoc') { - $result = (array)$this->last_result->records[$this->last_row_returned]; - } + /** + * Returns the next row for the result set after executing this statement. + * Rows can be fetched to contain columns as names or positions. If no + * rows are left in result set, this method will return false + * + * ### Example: + * + * ``` + * $statement = $connection->prepare('SELECT id, title from articles'); + * $statement->execute(); + * print_r($statement->fetch('assoc')); // will show ['id' => 1, 'title' => 'a title'] + * ``` + * + * @param string $type 'num' for positional columns, 'assoc' for named columns + * @return mixed Result array containing columns and values or false if no results + * are left + */ + public function fetch($type = 'num') { + if ($type === 'num') { + $result = (array)$this->last_result->records[$this->last_row_returned]; + } + if ($type === 'assoc') { + $result = (array)$this->last_result->records[$this->last_row_returned]; + } - $this->last_row_returned++; - return $result; - } + $this->last_row_returned++; + return $result; + } - /** - * Returns the error code for the last error that occurred when executing this statement. - * - * @return int|string - */ - public function errorCode() - { - return '00000'; - } + /** + * Returns the error code for the last error that occurred when executing this statement. + * + * @return int|string + */ + public function errorCode() { + return '00000'; + } - /** - * Returns the error information for the last error that occurred when executing - * this statement. - * - * @return array - */ - public function errorInfo() - { - return 'Salesforce Datasource doesnt produce PDO error codes - exceptions are usually thrown'; - } + /** + * Returns the error information for the last error that occurred when executing + * this statement. + * + * @return array + */ + public function errorInfo() { + return 'Salesforce Datasource doesnt produce PDO error codes - exceptions are usually thrown'; + } - /** - * Closes a cursor in the database, freeing up any resources and memory - * allocated to it. In most cases you don't need to call this method, as it is - * automatically called after fetching all results from the result set. - * - * @return void - */ - public function closeCursor() - { - return true; - } + /** + * Closes a cursor in the database, freeing up any resources and memory + * allocated to it. In most cases you don't need to call this method, as it is + * automatically called after fetching all results from the result set. + * + * @return void + */ + public function closeCursor() { + return true; + } - /** - * Returns the latest primary inserted using this statement. - * - * @param string|null $table table name or sequence to get last insert value from - * @param string|null $column the name of the column representing the primary key - * @return string - */ - public function lastInsertId($table = null, $column = null) - { - return $this->_last_insert_id[$table]; - } + /** + * Returns the latest primary inserted using this statement. + * + * @param string|null $table table name or sequence to get last insert value from + * @param string|null $column the name of the column representing the primary key + * @return string + */ + public function lastInsertId($table = null, $column = null) { + return $this->_last_insert_id[$table]; + } } From b900fc90df788319e2651b4d6036e2a9e9ce5d10 Mon Sep 17 00:00:00 2001 From: codemile Date: Thu, 19 Dec 2019 21:55:29 -0500 Subject: [PATCH 04/21] adds support for epilog to trigger queryAll instead of query --- src/Database/Driver/SalesforceDriverTrait.php | 5 +++++ src/Database/Statement/SalesforceStatement.php | 9 ++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Database/Driver/SalesforceDriverTrait.php b/src/Database/Driver/SalesforceDriverTrait.php index 1e1de03..89a1972 100644 --- a/src/Database/Driver/SalesforceDriverTrait.php +++ b/src/Database/Driver/SalesforceDriverTrait.php @@ -12,6 +12,11 @@ trait SalesforceDriverTrait protected $_connection; public $config; + /** + * @var \SforceEnterpriseClient + */ + public $client; + /** * Establishes a connection to the salesforce server * diff --git a/src/Database/Statement/SalesforceStatement.php b/src/Database/Statement/SalesforceStatement.php index 3214cae..a6ec0e8 100644 --- a/src/Database/Statement/SalesforceStatement.php +++ b/src/Database/Statement/SalesforceStatement.php @@ -20,6 +20,7 @@ /** * Statement class meant to be used by a Mysql PDO driver * + * @property \Cake\Database\Driver|\Salesforce\Database\Driver\SalesforceDriverTrait $_driver * @internal */ class SalesforceStatement extends StatementDecorator { @@ -86,7 +87,13 @@ public function execute($params = null) { throw new \Exception('Unsupported delete query. Only where ID clauses are supported'); } } else { - $result = $this->_driver->client->query($this->_interpolate($sql, $bindings)); + // Use epilog('@queryAll') with CakePHP's query build to include deleted records. + if (substr($sql, -10) === ' @queryAll') { + $sql = substr($sql, 0, strlen($sql) - 10); + $result = $this->_driver->client->queryAll($this->_interpolate($sql, $bindings)); + } else { + $result = $this->_driver->client->query($this->_interpolate($sql, $bindings)); + } } $this->last_rows_affected = $result->size; From 8a3935618185b1f78dfc3486fe4fba8ddd019c13 Mon Sep 17 00:00:00 2001 From: gregs Date: Fri, 7 Feb 2020 11:41:03 -0500 Subject: [PATCH 05/21] Fix logging --- src/Database/Driver/SalesforceDriverTrait.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Database/Driver/SalesforceDriverTrait.php b/src/Database/Driver/SalesforceDriverTrait.php index 89a1972..606c621 100644 --- a/src/Database/Driver/SalesforceDriverTrait.php +++ b/src/Database/Driver/SalesforceDriverTrait.php @@ -50,9 +50,9 @@ protected function _connect($dns, array $config) $sflogin = array('sessionId' => $mylogin->sessionId, 'serverUrl' => $mylogin->serverUrl); Cache::write($cache_key, $sflogin, 'salesforce'); } catch (\Exception $e) { - $this->log("Error logging into salesforce - Salesforce down?"); - $this->log("Username: " . $this->config['username']); - $this->log("Password: " . $this->config['password']); + \Cake\Cake\Log::write('error', "Error logging into salesforce - Salesforce down?"); + \Cake\Cake\Log::write('error', "Username: " . $this->config['username']); + \Cake\Cake\Log::write('error', "Password: " . $this->config['password']); } } From 25c1157c38339acbbf34bc35538274ccc6305890 Mon Sep 17 00:00:00 2001 From: gregs Date: Fri, 7 Feb 2020 11:42:26 -0500 Subject: [PATCH 06/21] Improved error handling --- src/Database/Statement/SalesforceStatement.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Database/Statement/SalesforceStatement.php b/src/Database/Statement/SalesforceStatement.php index a6ec0e8..1ef0d64 100644 --- a/src/Database/Statement/SalesforceStatement.php +++ b/src/Database/Statement/SalesforceStatement.php @@ -37,6 +37,7 @@ class SalesforceStatement extends StatementDecorator { public function execute($params = null) { $sql = $this->_statement->sql(); $bindings = $this->_statement->valueBinder()->bindings(); + $this->_driver->errors = null; //intercept Update here if ($this->_statement->type() == 'update') { @@ -234,6 +235,9 @@ public function fetch($type = 'num') { * @return int|string */ public function errorCode() { + if (!empty($this->_driver->errors)) { + return $this->_driver->errors[0]->statusCode; + } return '00000'; } @@ -244,6 +248,9 @@ public function errorCode() { * @return array */ public function errorInfo() { + if (!empty($this->_driver->errors)) { + return $this->_driver->errors[0]->message; + } return 'Salesforce Datasource doesnt produce PDO error codes - exceptions are usually thrown'; } From 8599a2b1001d137748bf204b4411b30aa567d39a Mon Sep 17 00:00:00 2001 From: gregs Date: Thu, 9 Apr 2020 12:22:23 -0400 Subject: [PATCH 07/21] Boolean variables in queries need to be the literal strings 'true' or 'false', not quoted --- src/Database/Statement/SalesforceStatement.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Database/Statement/SalesforceStatement.php b/src/Database/Statement/SalesforceStatement.php index 1ef0d64..51ed036 100644 --- a/src/Database/Statement/SalesforceStatement.php +++ b/src/Database/Statement/SalesforceStatement.php @@ -135,8 +135,9 @@ protected function _buildObjectFromUpdate($sql, $bindings) { protected function _replacement($binding, $quote = false) { switch ($binding['type']) { - case 'integer': case 'boolean': + return $binding['value'] ? 'true' : 'false'; + case 'integer': return (int)$binding['value']; case 'float': return (float)$binding['value']; From ea3bd336594901793d4e0b207c1d470641092847 Mon Sep 17 00:00:00 2001 From: Yevgeny Tomenko Date: Sun, 1 Aug 2021 18:18:31 +0300 Subject: [PATCH 08/21] step by step upgrade to 4.x --- .../Dialect/SalesforceDialectTrait.php | 1 + src/Database/Driver/Salesforce.php | 89 ++-- src/Database/Driver/SalesforceDriverTrait.php | 97 ++-- src/Database/SalesforceConnection.php | 53 +- src/Database/SalesforceQuery.php | 56 +- src/Database/SalesforceQueryCompiler.php | 53 +- src/Database/SalesforceType.php | 10 +- src/Database/Schema/SalesforceCollection.php | 11 +- src/Database/Schema/SalesforceSchema.php | 142 +++-- .../Statement/SalesforceStatement.php | 491 +++++++++--------- src/Database/Type/SalesforceStringType.php | 5 +- src/Model/Entity/Salesforce.php | 1 + src/Model/Table/SalesforceContactTable.php | 14 +- src/Model/Table/SalesforcesTable.php | 136 +++-- src/ORM/SalesforceMarshaller.php | 49 +- src/ORM/SalesforceQuery.php | 38 +- src/ORM/SalesforceResultSet.php | 68 +-- src/ORM/SalesforceTable.php | 68 ++- 18 files changed, 685 insertions(+), 697 deletions(-) diff --git a/src/Database/Dialect/SalesforceDialectTrait.php b/src/Database/Dialect/SalesforceDialectTrait.php index 5633fef..063fbed 100644 --- a/src/Database/Dialect/SalesforceDialectTrait.php +++ b/src/Database/Dialect/SalesforceDialectTrait.php @@ -12,6 +12,7 @@ * @since 3.0.0 * @license http://www.opensource.org/licenses/mit-license.php MIT License */ + namespace Salesforce\Database\Dialect; use Salesforce\Database\Schema\SalesforceSchema; diff --git a/src/Database/Driver/Salesforce.php b/src/Database/Driver/Salesforce.php index dae8265..a3b3069 100644 --- a/src/Database/Driver/Salesforce.php +++ b/src/Database/Driver/Salesforce.php @@ -12,9 +12,12 @@ * @since 3.0.0 * @license http://www.opensource.org/licenses/mit-license.php MIT License */ + namespace Salesforce\Database\Driver; use Cake\Database\Query; +use Cake\Database\QueryCompiler; +use Cake\Database\StatementInterface; use Cake\Database\ValueBinder; use Salesforce\Database\Dialect\SalesforceDialectTrait; use Salesforce\Database\Driver\SalesforceDriverTrait; @@ -46,75 +49,65 @@ class Salesforce extends Driver 'init' => [], ]; - /** - * Establishes a connection to the database server - * - * @return bool true on success - * @throws \ErrorException - */ - public function connect() - { - if ($this->_connection) { - return true; - } - $config = $this->_config; - - $this->_connect('', $config); - - if (!empty($config['init'])) { - $connection = $this->connection(); - foreach ((array)$config['init'] as $command) { - $connection->exec($command); - } - } - return true; - } - /** * Returns whether php is able to use this driver for connecting to database * * @return bool true if it is valid to use this driver */ - public function enabled() + public function enabled(): bool { - return true; //Dont know if I need this? + return true; } - /** - * Prepares a sql statement to be executed - * - * @param string|\Cake\Database\Query $query The query to prepare. - * @return \Cake\Database\StatementInterface - * @throws \ErrorException - */ - public function prepare($query) + /** + * Prepares a sql statement to be executed + * + * @param string|\Cake\Database\Query $query The query to prepare. + * @return \Cake\Database\StatementInterface + * @throws \ErrorException + */ + public function prepare($query): StatementInterface { $this->connect(); $isObject = $query instanceof SalesforceQuery; //$statement = $this->_connection->prepare($isObject ? $query->sql() : $query); $result = new SalesforceStatement($query, $this); - if ($isObject && $query->bufferResults() === false) { + if ($isObject && $query->isBufferedResultsEnabled() === false) { $result->bufferResults(false); } return $result; } /** - * {@inheritDoc} + * Establishes a connection to the database server + * + * @return bool true on success + * @throws \ErrorException */ - public function supportsDynamicConstraints() + public function connect(): bool { + if ($this->_connection) { + return true; + } + $config = $this->_config; + + $this->_connect('', $config); + + if (!empty($config['init'])) { + $connection = $this->connection(); + foreach ((array)$config['init'] as $command) { + $connection->exec($command); + } + } return true; } /** - * Returns an instance of a QueryCompiler - * - * @return \Cake\Database\QueryCompiler + * {@inheritDoc} */ - public function newCompiler() + public function supportsDynamicConstraints(): bool { - return new SalesforceQueryCompiler; + return true; } /** @@ -126,11 +119,21 @@ public function newCompiler() * @return array containing 2 entries. The first entity is the transformed query * and the second one the compiled SQL */ - public function compileQuery(Query $query, ValueBinder $generator) + public function compileQuery(Query $query, ValueBinder $generator): array { $processor = $this->newCompiler(); $translator = $this->queryTranslator($query->type()); $query = $translator($query); return [$query, $processor->compile($query, $generator)]; } + + /** + * Returns an instance of a QueryCompiler + * + * @return SalesforceQueryCompiler + */ + public function newCompiler(): QueryCompiler + { + return new SalesforceQueryCompiler(); + } } diff --git a/src/Database/Driver/SalesforceDriverTrait.php b/src/Database/Driver/SalesforceDriverTrait.php index 606c621..850ee18 100644 --- a/src/Database/Driver/SalesforceDriverTrait.php +++ b/src/Database/Driver/SalesforceDriverTrait.php @@ -4,62 +4,21 @@ use Cake\Database\Query; use Cake\Cache\Cache; +use Cake\Log\Log; + /** * SF driver trait */ trait SalesforceDriverTrait { - protected $_connection; public $config; - /** - * @var \SforceEnterpriseClient - */ + /** + * @var \SforceEnterpriseClient + */ public $client; - /** - * Establishes a connection to the salesforce server - * - * @param string $dsn A Driver-specific PDO-DSN - * @param array $config configuration to be used for creating connection - * @return bool true on success - * @throws \ErrorException - */ - protected function _connect($dns, array $config) - { - $this->config = $config; - - if (empty($this->config['my_wsdl'])) { - throw new \ErrorException ("A WSDL needs to be provided"); - } else { - $wsdl = CONFIG .DS . $this->config['my_wsdl']; - } - - $mySforceConnection = new \SforceEnterpriseClient(); - $mySforceConnection->createConnection($wsdl); - - $cache_key = $config['name'] . '_login'; - $sflogin = (array)Cache::read($cache_key, 'salesforce'); - - if(!empty($sflogin['sessionId'])) { - $mySforceConnection->setSessionHeader($sflogin['sessionId']); - $mySforceConnection->setEndPoint($sflogin['serverUrl']); - } else { - try{ - $mylogin = $mySforceConnection->login($this->config['username'], $this->config['password']); - $sflogin = array('sessionId' => $mylogin->sessionId, 'serverUrl' => $mylogin->serverUrl); - Cache::write($cache_key, $sflogin, 'salesforce'); - } catch (\Exception $e) { - \Cake\Cake\Log::write('error', "Error logging into salesforce - Salesforce down?"); - \Cake\Cake\Log::write('error', "Username: " . $this->config['username']); - \Cake\Cake\Log::write('error', "Password: " . $this->config['password']); - } - } - - $this->client = $mySforceConnection; - $this->connected = true; - return $this->connected; - } + protected $_connection; /** * Returns correct connection resource or object that is internally used @@ -177,4 +136,48 @@ public function supportsQuoting() { return false; } + + /** + * Establishes a connection to the salesforce server + * + * @param string $dsn A Driver-specific PDO-DSN + * @param array $config configuration to be used for creating connection + * @return bool true on success + * @throws \ErrorException + */ + protected function _connect($dns, array $config) + { + $this->config = $config; + + if (empty($this->config['my_wsdl'])) { + throw new \ErrorException ("A WSDL needs to be provided"); + } else { + $wsdl = CONFIG . DS . $this->config['my_wsdl']; + } + + $mySforceConnection = new \SforceEnterpriseClient(); + $mySforceConnection->createConnection($wsdl); + + $cache_key = $config['name'] . '_login'; + $sflogin = (array)Cache::read($cache_key, 'salesforce'); + + if (!empty($sflogin['sessionId'])) { + $mySforceConnection->setSessionHeader($sflogin['sessionId']); + $mySforceConnection->setEndPoint($sflogin['serverUrl']); + } else { + try { + $mylogin = $mySforceConnection->login($this->config['username'], $this->config['password']); + $sflogin = array('sessionId' => $mylogin->sessionId, 'serverUrl' => $mylogin->serverUrl); + Cache::write($cache_key, $sflogin, 'salesforce'); + } catch (\Exception $e) { + Log::write('error', "Error logging into salesforce - Salesforce down?"); + Log::write('error', "Username: " . $this->config['username']); + Log::write('error', "Password: " . $this->config['password']); + } + } + + $this->client = $mySforceConnection; + $this->connected = true; + return $this->connected; + } } diff --git a/src/Database/SalesforceConnection.php b/src/Database/SalesforceConnection.php index 7a32095..e3e0241 100644 --- a/src/Database/SalesforceConnection.php +++ b/src/Database/SalesforceConnection.php @@ -12,22 +12,13 @@ * @since 3.0.0 * @license http://www.opensource.org/licenses/mit-license.php MIT License */ + namespace Salesforce\Database; use Cake\Database\Query; +use Cake\Database\StatementInterface; use Cake\Database\ValueBinder; -use Salesforce\Database\Schema\SalesforceCollection as SchemaCollection; -use Cake\Database\TypeConverterTrait; use Cake\Database\Connection; -use Cake\Database\Exception\MissingConnectionException; -use Cake\Database\Exception\MissingDriverException; -use Cake\Database\Exception\MissingExtensionException; -use Cake\Database\Log\LoggedQuery; -use Cake\Database\Log\LoggingStatement; -use Cake\Database\Log\QueryLogger; -use Cake\Database\Schema\CachedCollection; -use Cake\Datasource\ConnectionInterface; -use Exception; /** * Represents a connection with a database server. @@ -35,55 +26,47 @@ class SalesforceConnection extends Connection { /** - * Compiles a Query object into a SQL string according to the dialect for this - * connection's driver - * - * @param Query $query The query to be compiled - * @param ValueBinder $generator The placeholder generator to use - * @return string + * {@inheritDoc} */ - public function compileQuery(Query $query, ValueBinder $generator) + public function compileQuery(Query $query, ValueBinder $binder): string { - return $this->driver()->compileQuery($query, $generator)[1]; + return $this->getDriver() + ->compileQuery($query, $binder)[1]; } /** - * Create a new Query instance for this connection. - * - * @return Query + * {@inheritDoc} */ - public function newQuery() + public function newQuery(): Query { return new SalesforceQuery($this); } - public function run(Query $query) + /** + * {@inheritDoc} + */ + public function run(Query $query): StatementInterface { $statement = $this->prepare($query); - $query->valueBinder()->attachTo($statement); + $query->getValueBinder() + ->attachTo($statement); $statement->execute(); return $statement; } /** - * Checks if the driver supports quoting. - * - * @return bool + * {@inheritDoc} */ - public function supportsQuoting() + public function supportsQuoting(): bool { return false; } /** - * Quotes a database identifier (a column name, table name, etc..) to - * be used safely in queries without the risk of using reserved words. - * - * @param string $identifier The identifier to quote. - * @return string + * {@inheritDoc} */ - public function quoteIdentifier($identifier) + public function quoteIdentifier($identifier): string { return $identifier; } diff --git a/src/Database/SalesforceQuery.php b/src/Database/SalesforceQuery.php index 66f61bd..7845e89 100644 --- a/src/Database/SalesforceQuery.php +++ b/src/Database/SalesforceQuery.php @@ -12,6 +12,7 @@ * @since 3.0.0 * @license http://www.opensource.org/licenses/mit-license.php MIT License */ + namespace Salesforce\Database; use Cake\Database\Query; @@ -32,32 +33,6 @@ */ class SalesforceQuery extends Query { - /** - * Returns the SQL representation of this object. - * - * This function will compile this query to make it compatible - * with the SQL dialect that is used by the connection, This process might - * add, remove or alter any query part or internal expression to make it - * executable in the target platform. - * - * The resulting query may have placeholders that will be replaced with the actual - * values when the query is executed, hence it is most suitable to use with - * prepared statements. - * - * @param ValueBinder $generator A placeholder object that will hold - * associated values for expressions - * @return string - */ - public function sql(ValueBinder $generator = null, $incoming = null) - { - if (!$generator) { - $generator = $incoming->valueBinder(); - $generator->resetCount(); - } - - return $incoming->connection()->compileQuery($incoming, $generator); - } - /** * Will iterate over every specified part. Traversing functions can aggregate * results using variables in the closure or instance variables. This function @@ -94,8 +69,35 @@ public function traverse(callable $visitor, array $parts = []) * * @return string */ - public function __toString() + public function __toString(): string { return $this->sql(); } + + /** + * Returns the SQL representation of this object. + * + * This function will compile this query to make it compatible + * with the SQL dialect that is used by the connection, This process might + * add, remove or alter any query part or internal expression to make it + * executable in the target platform. + * + * The resulting query may have placeholders that will be replaced with the actual + * values when the query is executed, hence it is most suitable to use with + * prepared statements. + * + * @param ValueBinder $generator A placeholder object that will hold + * associated values for expressions + * @return string + */ + public function sql(ValueBinder $generator = null, $incoming = null) + { + if (!$generator) { + $generator = $incoming->valueBinder(); + $generator->resetCount(); + } + + return $incoming->connection() + ->compileQuery($incoming, $generator); + } } \ No newline at end of file diff --git a/src/Database/SalesforceQueryCompiler.php b/src/Database/SalesforceQueryCompiler.php index d96a2e1..d0119c2 100644 --- a/src/Database/SalesforceQueryCompiler.php +++ b/src/Database/SalesforceQueryCompiler.php @@ -12,6 +12,7 @@ * @since 3.0.0 * @license http://www.opensource.org/licenses/mit-license.php MIT License */ + namespace Salesforce\Database; use Cake\Database\Query; @@ -26,19 +27,24 @@ class SalesforceQueryCompiler extends QueryCompiler { /** - * Helper function used to build the string representation of a SELECT clause, - * it constructs the field list taking care of aliasing and - * converting expression objects to string. This function also constructs the - * DISTINCT clause for the query. This has been modified to allow it to support Salesforce - * - * @param array $parts list of fields to be transformed to string - * @param \Cake\Database\Query $query The query that is being compiled - * @param \Cake\Database\ValueBinder $generator the placeholder generator to be used in expressions - * @return string + * {@inheritDoc} + */ + public function compile(Query $query, ValueBinder $binder): string + { + $sql = ''; + $type = $query->type(); + $query->traverseParts($this->_sqlCompiler($sql, $query, $binder), $this->{'_' . $type . 'Parts'}); + + return $sql; + } + + /** + * {@inheritDoc} */ - protected function _buildSelectPart($parts, $query, $generator) + protected function _buildSelectPart(array $parts, Query $query, ValueBinder $binder): string { - $driver = $query->connection()->driver(); + $driver = $query->getConnection() + ->getDriver(); $select = 'SELECT %s%s%s'; if ($this->_orderedUnion && $query->clause('union')) { $select = '(SELECT %s%s%s'; @@ -47,7 +53,7 @@ protected function _buildSelectPart($parts, $query, $generator) $modifiers = $query->clause('modifier') ?: null; $normalized = []; - $parts = $this->_stringifyExpressions($parts, $generator); + $parts = $this->_stringifyExpressions($parts, $binder); foreach ($parts as $k => $p) { if (!is_numeric($k)) { $p = $p; //Leave it alone @@ -60,33 +66,14 @@ protected function _buildSelectPart($parts, $query, $generator) } if (is_array($distinct)) { - $distinct = $this->_stringifyExpressions($distinct, $generator); + $distinct = $this->_stringifyExpressions($distinct, $binder); $distinct = sprintf('DISTINCT ON (%s) ', implode(', ', $distinct)); } if ($modifiers !== null) { - $modifiers = $this->_stringifyExpressions($modifiers, $generator); + $modifiers = $this->_stringifyExpressions($modifiers, $binder); $modifiers = implode(' ', $modifiers) . ' '; } return sprintf($select, $distinct, $modifiers, implode(', ', $normalized)); } - - /** - * Returns the SQL representation of the provided query after generating - * the placeholders for the bound values using the provided generator - * - * @param \Cake\Database\Query $query The query that is being compiled - * @param \Cake\Database\ValueBinder $generator the placeholder generator to be used in expressions - * @return \Closure - */ - public function compile(Query $query, ValueBinder $generator) - { - $sql = ''; - $type = $query->type(); - $query->traverse( - $this->_sqlCompiler($sql, $query, $generator), - $this->{'_' . $type . 'Parts'} - ); - return $sql; - } } \ No newline at end of file diff --git a/src/Database/SalesforceType.php b/src/Database/SalesforceType.php index b803e4d..1dbf7c0 100644 --- a/src/Database/SalesforceType.php +++ b/src/Database/SalesforceType.php @@ -12,9 +12,11 @@ * @since 3.0.0 * @license http://www.opensource.org/licenses/mit-license.php MIT License */ + namespace Salesforce\Database; use Cake\Database\Type; +use Cake\Database\TypeInterface; use InvalidArgumentException; /** @@ -63,13 +65,9 @@ class SalesforceType extends Type ]; /** - * Returns a Type object capable of converting a type identified by $name - * - * @param string $name type identifier - * @throws \InvalidArgumentException If type identifier is unknown - * @return Type + * {@inheritDoc} */ - public static function build($name) + public static function build(string $name): TypeInterface { //force rebuild of string type if ($name != "string") { diff --git a/src/Database/Schema/SalesforceCollection.php b/src/Database/Schema/SalesforceCollection.php index 9647965..fd19465 100644 --- a/src/Database/Schema/SalesforceCollection.php +++ b/src/Database/Schema/SalesforceCollection.php @@ -12,11 +12,13 @@ * @since 3.0.0 * @license http://www.opensource.org/licenses/mit-license.php MIT License */ + namespace Salesforce\Database\Schema; use Cake\Database\Exception; use Cake\Database\Schema\Collection; use Cake\Database\Schema\Table; +use Cake\Database\Schema\TableSchemaInterface; use Cake\Datasource\ConnectionInterface; use PDOException; @@ -34,7 +36,7 @@ class SalesforceCollection extends Collection * * @return array The list of tables in the connected database/schema. */ - public function listTables() + public function listTables(): array { list($sql, $params) = $this->_dialect->listTables($this->_connection->config()); $result = []; @@ -59,16 +61,17 @@ public function listTables() * * @param string $name The name of the table to describe. * @param array $options The options to use, see above. - * @return \Cake\Database\Schema\Table Object with column metadata. + * @return \Cake\Database\Schema\TableSchemaInterface Object with column metadata. * @throws \Cake\Database\Exception when table cannot be described. */ - public function describe($name, array $options = []) + public function describe($name, array $options = []): TableSchemaInterface { $config = $this->_connection->config(); if (strpos($name, '.')) { list($config['schema'], $name) = explode('.', $name); } - $table = new Table($name); + $table = $this->_connection->getDriver() + ->newTableSchema($name); $schema = $this->_dialect->describeColumn($name, $config); diff --git a/src/Database/Schema/SalesforceSchema.php b/src/Database/Schema/SalesforceSchema.php index 2b0278b..e2f2b01 100644 --- a/src/Database/Schema/SalesforceSchema.php +++ b/src/Database/Schema/SalesforceSchema.php @@ -12,12 +12,13 @@ * @since 3.0.0 * @license http://www.opensource.org/licenses/mit-license.php MIT License */ + namespace Salesforce\Database\Schema; +use Cake\Database\Schema\TableSchema; use Salesforce\Database\Driver\SalesforceDriverTrait; use Cake\Database\Exception; use Cake\Database\Schema\BaseSchema; -use Cake\Database\Schema\Table; /** * Schema generation/reflection features for MySQL @@ -25,13 +26,14 @@ class SalesforceSchema extends BaseSchema { - use SalesforceDriverTrait; + use SalesforceDriverTrait; + /** * {@inheritDoc} */ - public function listTablesSql($config) + public function listTablesSql($config): array { - return false; + return []; } /** @@ -45,7 +47,7 @@ public function listTables() } /** - * Custom function that queries the datasource for the table list as SOQL doesn't accept + * Custom function that queries the datasource for the table list as SQL doesn't accept * DESCRIBE functionality */ public function describeColumn($tableName, $config) @@ -55,8 +57,6 @@ public function describeColumn($tableName, $config) foreach ($sfschema->fields as $field) { switch ($field->type) { case "id": - $field->type = "integer"; - break; case "integer": $field->type = "integer"; break; @@ -79,7 +79,12 @@ public function describeColumn($tableName, $config) //Capital letters here as it is emulating return from MySQL if ($field->length > 0) { - $newSchema[] = array('Field' => $field->name, 'Type' => $field->type, 'Length' => $field->length, 'Null' => $field->nillable); + $newSchema[] = array( + 'Field' => $field->name, + 'Type' => $field->type, + 'Length' => $field->length, + 'Null' => $field->nillable + ); } else { $newSchema[] = array('Field' => $field->name, 'Type' => $field->type, 'Null' => $field->nillable); } @@ -87,41 +92,57 @@ public function describeColumn($tableName, $config) return $newSchema; } + /** * {@inheritDoc} */ - public function describeColumnSql($tableName, $config) + public function describeColumnSql($tableName, $config): array { - return false; + return []; } /** * {@inheritDoc} */ - public function describeIndexSql($tableName, $config) + public function describeIndexSql($tableName, $config): array { - return false; + return []; } /** * {@inheritDoc} */ - public function describeOptionsSql($tableName, $config) + public function describeOptionsSql($tableName, $config): array { - return false; + return []; } /** * {@inheritDoc} */ - public function convertOptionsDescription(Table $table, $row) + public function convertOptionsDescription(TableSchema $table, $row): void { - $table->options([ + $table->setOptions([ 'engine' => $row['Engine'], 'collation' => $row['Collation'], ]); } + /** + * {@inheritDoc} + */ + public function convertColumnDescription(TableSchema $table, $row): void + { + $field = $this->_convertColumn($row['Type']); + $field += [ + 'null' => $row['Null'] === 'YES' ? true : false + ]; + if (isset($row['Extra']) && $row['Extra'] === 'auto_increment') { + $field['autoIncrement'] = true; + } + $table->addColumn($row['Field'], $field); + } + /** * Convert a MySQL column type into an abstract type. * @@ -200,64 +221,47 @@ protected function _convertColumn($column) /** * {@inheritDoc} */ - public function convertColumnDescription(Table $table, $row) + public function convertIndexDescription(TableSchema $table, $row): void { - $field = $this->_convertColumn($row['Type']); - $field += [ - 'null' => $row['Null'] === 'YES' ? true : false - ]; - if (isset($row['Extra']) && $row['Extra'] === 'auto_increment') { - $field['autoIncrement'] = true; - } - $table->addColumn($row['Field'], $field); } /** * {@inheritDoc} */ - public function convertIndexDescription(Table $table, $row) + public function describeForeignKeySql($tableName, $config): array { - return false; + return []; } /** * {@inheritDoc} */ - public function describeForeignKeySql($tableName, $config) + public function convertForeignKeyDescription(TableSchema $table, $row): void { - return false; } /** * {@inheritDoc} */ - public function convertForeignKeyDescription(Table $table, $row) + public function truncateTableSql(TableSchema $table): array { - return false; + return []; } /** * {@inheritDoc} */ - public function truncateTableSql(Table $table) + public function createTableSql(TableSchema $table, $columns, $constraints, $indexes): array { - return false; + return []; } /** * {@inheritDoc} */ - public function createTableSql(Table $table, $columns, $constraints, $indexes) + public function columnSql(TableSchema $table, $name): string { - return false; - } - - /** - * {@inheritDoc} - */ - public function columnSql(Table $table, $name) - { - $data = $table->column($name); + $data = $table->getColumn($name); $out = $this->_driver->quoteIdentifier($name); $typeMap = [ 'integer' => ' INTEGER', @@ -295,29 +299,21 @@ public function columnSql(Table $table, $name) } $hasPrecision = ['float', 'decimal']; - if (in_array($data['type'], $hasPrecision, true) && - (isset($data['length']) || isset($data['precision'])) - ) { + if (in_array($data['type'], $hasPrecision, true) && (isset($data['length']) || isset($data['precision']))) { $out .= '(' . (int)$data['length'] . ',' . (int)$data['precision'] . ')'; } $hasUnsigned = ['float', 'decimal', 'integer', 'biginteger']; - if (in_array($data['type'], $hasUnsigned, true) && - isset($data['unsigned']) && $data['unsigned'] === true - ) { + if (in_array($data['type'], $hasUnsigned, true) && isset($data['unsigned']) && $data['unsigned'] === true) { $out .= ' UNSIGNED'; } if (isset($data['null']) && $data['null'] === false) { $out .= ' NOT NULL'; } - $addAutoIncrement = ( - [$name] == (array)$table->primaryKey() && - !$table->hasAutoIncrement() - ); - if (in_array($data['type'], ['integer', 'biginteger']) && - ($data['autoIncrement'] === true || $addAutoIncrement) - ) { + $addAutoIncrement = ([$name] == (array)$table->getPrimaryKey() && !$table->hasAutoIncrement()); + if (in_array($data['type'], + ['integer', 'biginteger']) && ($data['autoIncrement'] === true || $addAutoIncrement)) { $out .= ' AUTO_INCREMENT'; } if (isset($data['null']) && $data['null'] === true) { @@ -328,10 +324,8 @@ public function columnSql(Table $table, $name) $out .= ' DEFAULT ' . $this->_driver->schemaValue($data['default']); unset($data['default']); } - if (isset($data['default']) && - in_array($data['type'], ['timestamp', 'datetime']) && - strtolower($data['default']) === 'current_timestamp' - ) { + if (isset($data['default']) && in_array($data['type'], + ['timestamp', 'datetime']) && strtolower($data['default']) === 'current_timestamp') { $out .= ' DEFAULT CURRENT_TIMESTAMP'; unset($data['default']); } @@ -344,15 +338,7 @@ public function columnSql(Table $table, $name) /** * {@inheritDoc} */ - public function constraintSql(Table $table, $name) - { - return false; - } - - /** - * {@inheritDoc} - */ - public function addConstraintSql(Table $table) + public function constraintSql(TableSchema $table, $name): string { return false; } @@ -360,28 +346,24 @@ public function addConstraintSql(Table $table) /** * {@inheritDoc} */ - public function dropConstraintSql(Table $table) + public function addConstraintSql(TableSchema $table): array { - return false; + return []; } /** * {@inheritDoc} */ - public function indexSql(Table $table, $name) + public function dropConstraintSql(TableSchema $table): array { - return false; + return []; } /** - * Helper method for generating key SQL snippets. - * - * @param string $prefix The key prefix - * @param array $data Key data. - * @return string + * {@inheritDoc} */ - protected function _keySql($prefix, $data) + public function indexSql(TableSchema $table, $name): string { - return false; + return ''; } } diff --git a/src/Database/Statement/SalesforceStatement.php b/src/Database/Statement/SalesforceStatement.php index 51ed036..78443c5 100644 --- a/src/Database/Statement/SalesforceStatement.php +++ b/src/Database/Statement/SalesforceStatement.php @@ -12,6 +12,7 @@ * @since 3.0.0 * @license http://www.opensource.org/licenses/mit-license.php MIT License */ + namespace Salesforce\Database\Statement; use AssignmentRuleHeader; @@ -23,257 +24,281 @@ * @property \Cake\Database\Driver|\Salesforce\Database\Driver\SalesforceDriverTrait $_driver * @internal */ -class SalesforceStatement extends StatementDecorator { +class SalesforceStatement extends StatementDecorator +{ + + protected $last_rows_affected = 0; + + protected $last_result; //pretty sure this is awful! + + protected $last_row_returned = 0; + + private $_last_insert_id = []; - protected $last_rows_affected = 0; - protected $last_result; //pretty sure this is awful! - protected $last_row_returned = 0; - private $_last_insert_id = []; + /** + * {@inheritDoc} + * + * @throws \Exception + */ + public function execute(?array $params = null): bool + { + $sql = $this->_statement->sql(); + $bindings = $this->_statement->valueBinder() + ->bindings(); + $this->_driver->errors = null; - /** - * {@inheritDoc} - * - */ - public function execute($params = null) { - $sql = $this->_statement->sql(); - $bindings = $this->_statement->valueBinder()->bindings(); - $this->_driver->errors = null; + //intercept Update here + if ($this->_statement->type() == 'update') { + $results = $this->_driver->client->update([$this->_buildObjectFromUpdate($sql, $bindings)], + $this->_statement->repository()->name); + if (is_object($results)) { + trigger_error('Unexpected object results', E_USER_ERROR); + } + $result = new \stdClass(); + if ($results[0]->success) { + $result->size = 1; + } else { + $result->size = 0; + $this->_driver->errors = $results[0]->errors; + } + } else { + if ($this->_statement->type() == 'insert') { + $object = $this->_buildObjectFromInsert($sql, $bindings); + if (empty($object->OwnerId)) { + $header = new AssignmentRuleHeader(null, true); // run the default lead assignment rule + $this->_driver->client->setAssignmentRuleHeader($header); + } + $results = $this->_driver->client->create([$object], $this->_statement->repository()->name); + if (is_object($results)) { + trigger_error('Unexpected object results', E_USER_ERROR); + } + $result = new \stdClass(); + if ($results[0]->success) { + $result->size = 1; + $this->_last_insert_id[$this->_statement->repository()->name] = $results[0]->id; + } else { + $result->size = 0; + $this->_driver->errors = $results[0]->errors; + } + } else { + if ($this->_statement->type() == 'delete') { + if (count($bindings) == 1 && preg_match('/DELETE FROM .* WHERE Id = :c0/', $sql)) { + $id = $bindings[':c0']['value']; + $results = $this->_driver->client->delete([$id]); + if (is_object($results)) { + trigger_error('Unexpected object results', E_USER_ERROR); + } + $result = new \stdClass(); + if ($results[0]->success) { + $result->size = 1; + } else { + $result->size = 0; + $this->_driver->errors = $results[0]->errors; + } + } else { + throw new \Exception('Unsupported delete query. Only where ID clauses are supported'); + } + } else { + // Use epilog('@queryAll') with CakePHP's query build to include deleted records. + if (substr($sql, -10) === ' @queryAll') { + $sql = substr($sql, 0, strlen($sql) - 10); + $result = $this->_driver->client->queryAll($this->_interpolate($sql, $bindings)); + } else { + $result = $this->_driver->client->query($this->_interpolate($sql, $bindings)); + } + } + } + } - //intercept Update here - if ($this->_statement->type() == 'update') { - $results = $this->_driver->client->update([$this->_buildObjectFromUpdate($sql, $bindings)], $this->_statement->repository()->name); - if (is_object($results)) { - trigger_error('Unexpected object results', E_USER_ERROR); - } - $result = new \stdClass(); - if ($results[0]->success) { - $result->size = 1; - } else { - $result->size = 0; - $this->_driver->errors = $results[0]->errors; - } - } else if ($this->_statement->type() == 'insert') { - $object = $this->_buildObjectFromInsert($sql, $bindings); - if (empty($object->OwnerId)) { - $header = new AssignmentRuleHeader(null, true); // run the default lead assignment rule - $this->_driver->client->setAssignmentRuleHeader($header); - } - $results = $this->_driver->client->create([$object], $this->_statement->repository()->name); - if (is_object($results)) { - trigger_error('Unexpected object results', E_USER_ERROR); - } - $result = new \stdClass(); - if ($results[0]->success) { - $result->size = 1; - $this->_last_insert_id[$this->_statement->repository()->name] = $results[0]->id; - } else { - $result->size = 0; - $this->_driver->errors = $results[0]->errors; - } - } else if ($this->_statement->type() == 'delete') { - if (count($bindings) == 1 && preg_match('/DELETE FROM .* WHERE Id = :c0/', $sql)) { - $id = $bindings[':c0']['value']; - $results = $this->_driver->client->delete([$id]); - if (is_object($results)) { - trigger_error('Unexpected object results', E_USER_ERROR); - } - $result = new \stdClass(); - if ($results[0]->success) { - $result->size = 1; - } else { - $result->size = 0; - $this->_driver->errors = $results[0]->errors; - } - } else { - throw new \Exception('Unsupported delete query. Only where ID clauses are supported'); - } - } else { - // Use epilog('@queryAll') with CakePHP's query build to include deleted records. - if (substr($sql, -10) === ' @queryAll') { - $sql = substr($sql, 0, strlen($sql) - 10); - $result = $this->_driver->client->queryAll($this->_interpolate($sql, $bindings)); - } else { - $result = $this->_driver->client->query($this->_interpolate($sql, $bindings)); - } - } + $this->last_rows_affected = $result->size; + $this->last_result = $result; - $this->last_rows_affected = $result->size; - $this->last_result = $result; - return $result; - } + return $result; + } - /** - * Helper function used to build an sObject from an update query. - * - * @param string $sql The sql query - * @param array $bindings List of placeholder replacement values - * @return mixed - */ - protected function _buildObjectFromUpdate($sql, $bindings) { - preg_match('/UPDATE .* SET (.*) WHERE (.*)/', $sql, $parts); - $cleanedSQL = explode(' , ', $parts[1]); - $cleanedSQL[] = $parts[2]; - $newSQL = ['fieldsToNull' => []]; - foreach ($cleanedSQL as $row) { - [$fieldName, $value] = explode(' = ', $row); - $fieldName = trim($fieldName); - $value = $this->_replacement($bindings[trim($value)]); - if ($value === '') { - $newSQL['fieldsToNull'][] = $fieldName; - } else { - $newSQL[$fieldName] = $value; - } - } + /** + * Helper function used to build an sObject from an update query. + * + * @param string $sql The sql query + * @param array $bindings List of placeholder replacement values + * @return mixed + */ + protected function _buildObjectFromUpdate($sql, $bindings) + { + preg_match('/UPDATE .* SET (.*) WHERE (.*)/', $sql, $parts); + $cleanedSQL = explode(' , ', $parts[1]); + $cleanedSQL[] = $parts[2]; + $newSQL = ['fieldsToNull' => []]; + foreach ($cleanedSQL as $row) { + [$fieldName, $value] = explode(' = ', $row); + $fieldName = trim($fieldName); + $value = $this->_replacement($bindings[trim($value)]); + if ($value === '') { + $newSQL['fieldsToNull'][] = $fieldName; + } else { + $newSQL[$fieldName] = $value; + } + } - if (count($newSQL['fieldsToNull']) === 0) { - unset($newSQL['fieldsToNull']); - } + if (count($newSQL['fieldsToNull']) === 0) { + unset($newSQL['fieldsToNull']); + } - //return as object - return (object)$newSQL; - } + //return as object + return (object)$newSQL; + } - protected function _replacement($binding, $quote = false) { - switch ($binding['type']) { - case 'boolean': - return $binding['value'] ? 'true' : 'false'; - case 'integer': - return (int)$binding['value']; - case 'float': - return (float)$binding['value']; - case 'datetime': - case 'date': - $ret = (string)$binding['value']; - break; - case 'string': - $ret = trim($binding['value']); - break; - default: - $ret = addslashes(trim($binding['value'])); - break; - } - if ($quote) { - return "'$ret'"; - } - return $ret; - } + protected function _replacement($binding, $quote = false) + { + switch ($binding['type']) { + case 'boolean': + return $binding['value'] ? 'true' : 'false'; + case 'integer': + return (int)$binding['value']; + case 'float': + return (float)$binding['value']; + case 'datetime': + case 'date': + $ret = (string)$binding['value']; + break; + case 'string': + $ret = trim($binding['value']); + break; + default: + $ret = addslashes(trim($binding['value'])); + break; + } + if ($quote) { + return "'$ret'"; + } + return $ret; + } - /** - * Helper function used to build an sObject from an insert query. - * - * @param string $sql The sql query - * @param array $bindings List of placeholder replacement values - * @return mixed - */ - protected function _buildObjectFromInsert($sql, $bindings) { - preg_match('/\((.*)\) VALUES \((.*)\)/', $sql, $blobs); - $fields = explode(', ', $blobs[1]); - $placeholders = explode(', ', $blobs[2]); - $newSQL = []; - foreach ($fields as $key => $field) { - $newSQL[$field] = $this->_replacement($bindings[$placeholders[$key]]); - } + /** + * Helper function used to build an sObject from an insert query. + * + * @param string $sql The sql query + * @param array $bindings List of placeholder replacement values + * @return mixed + */ + protected function _buildObjectFromInsert($sql, $bindings) + { + preg_match('/\((.*)\) VALUES \((.*)\)/', $sql, $blobs); + $fields = explode(', ', $blobs[1]); + $placeholders = explode(', ', $blobs[2]); + $newSQL = []; + foreach ($fields as $key => $field) { + $newSQL[$field] = $this->_replacement($bindings[$placeholders[$key]]); + } - //remove empty / null values - $newSQL = array_filter($newSQL, 'strlen'); + //remove empty / null values + $newSQL = array_filter($newSQL, 'strlen'); - //return as object - return (object)$newSQL; - } + //return as object + return (object)$newSQL; + } - /** - * Helper function used to replace query placeholders by the real - * params used to execute the query. - * - * @param string $sql The sql query - * @param array $bindings List of placeholder replacement values - * @return string - */ - protected function _interpolate($sql, $bindings) { - foreach ($bindings as $binding) { - $sql = preg_replace('/:' . $binding['placeholder'] . '\b/i', $this->_replacement($binding, true), $sql); - } + /** + * Helper function used to replace query placeholders by the real + * params used to execute the query. + * + * @param string $sql The sql query + * @param array $bindings List of placeholder replacement values + * @return string + */ + protected function _interpolate($sql, $bindings) + { + foreach ($bindings as $binding) { + $sql = preg_replace('/:' . $binding['placeholder'] . '\b/i', $this->_replacement($binding, true), $sql); + } - return $sql; - } + return $sql; + } - public function rowCount() { - return $this->last_rows_affected; - } + public function rowCount(): int + { + return $this->last_rows_affected; + } - /** - * Returns the next row for the result set after executing this statement. - * Rows can be fetched to contain columns as names or positions. If no - * rows are left in result set, this method will return false - * - * ### Example: - * - * ``` - * $statement = $connection->prepare('SELECT id, title from articles'); - * $statement->execute(); - * print_r($statement->fetch('assoc')); // will show ['id' => 1, 'title' => 'a title'] - * ``` - * - * @param string $type 'num' for positional columns, 'assoc' for named columns - * @return mixed Result array containing columns and values or false if no results - * are left - */ - public function fetch($type = 'num') { - if ($type === 'num') { - $result = (array)$this->last_result->records[$this->last_row_returned]; - } - if ($type === 'assoc') { - $result = (array)$this->last_result->records[$this->last_row_returned]; - } + /** + * Returns the next row for the result set after executing this statement. + * Rows can be fetched to contain columns as names or positions. If no + * rows are left in result set, this method will return false + * + * ### Example: + * + * ``` + * $statement = $connection->prepare('SELECT id, title from articles'); + * $statement->execute(); + * print_r($statement->fetch('assoc')); // will show ['id' => 1, 'title' => 'a title'] + * ``` + * + * @param string $type 'num' for positional columns, 'assoc' for named columns + * @return mixed Result array containing columns and values or false if no results + * are left + */ + public function fetch($type = 'num') + { + if ($type === 'num') { + $result = (array)$this->last_result->records[$this->last_row_returned]; + } + if ($type === 'assoc') { + $result = (array)$this->last_result->records[$this->last_row_returned]; + } - $this->last_row_returned++; - return $result; - } + $this->last_row_returned++; + return $result; + } - /** - * Returns the error code for the last error that occurred when executing this statement. - * - * @return int|string - */ - public function errorCode() { - if (!empty($this->_driver->errors)) { - return $this->_driver->errors[0]->statusCode; - } - return '00000'; - } + /** + * Returns the error code for the last error that occurred when executing this statement. + * + * @return int|string + */ + public function errorCode() + { + if (!empty($this->_driver->errors)) { + return $this->_driver->errors[0]->statusCode; + } + return '00000'; + } - /** - * Returns the error information for the last error that occurred when executing - * this statement. - * - * @return array - */ - public function errorInfo() { - if (!empty($this->_driver->errors)) { - return $this->_driver->errors[0]->message; - } - return 'Salesforce Datasource doesnt produce PDO error codes - exceptions are usually thrown'; - } + /** + * Returns the error information for the last error that occurred when executing + * this statement. + * + * @return array + */ + public function errorInfo(): array + { + if (!empty($this->_driver->errors)) { + return $this->_driver->errors[0]->message; + } + return [ + 'Salesforce Datasource doesnt produce PDO error codes - exceptions are usually thrown' + ]; + } - /** - * Closes a cursor in the database, freeing up any resources and memory - * allocated to it. In most cases you don't need to call this method, as it is - * automatically called after fetching all results from the result set. - * - * @return void - */ - public function closeCursor() { - return true; - } + /** + * Closes a cursor in the database, freeing up any resources and memory + * allocated to it. In most cases you don't need to call this method, as it is + * automatically called after fetching all results from the result set. + * + * @return void + */ + public function closeCursor(): void + { + } - /** - * Returns the latest primary inserted using this statement. - * - * @param string|null $table table name or sequence to get last insert value from - * @param string|null $column the name of the column representing the primary key - * @return string - */ - public function lastInsertId($table = null, $column = null) { - return $this->_last_insert_id[$table]; - } + /** + * Returns the latest primary inserted using this statement. + * + * @param string|null $table table name or sequence to get last insert value from + * @param string|null $column the name of the column representing the primary key + * @return string + */ + public function lastInsertId(?string $table = null, ?string $column = null) + { + return $this->_last_insert_id[$table]; + } } diff --git a/src/Database/Type/SalesforceStringType.php b/src/Database/Type/SalesforceStringType.php index 6376b65..8ba61b9 100644 --- a/src/Database/Type/SalesforceStringType.php +++ b/src/Database/Type/SalesforceStringType.php @@ -12,6 +12,7 @@ * @since 3.1.2 * @license http://www.opensource.org/licenses/mit-license.php MIT License */ + namespace Salesforce\Database\Type; use Cake\Database\Type\StringType; @@ -29,7 +30,7 @@ class SalesforceStringType extends StringType * @param mixed $value The value to convert. * @return mixed Converted value. */ - public function marshal($value) + public function marshal($value): ?string { if ($value === null) { return null; @@ -38,7 +39,7 @@ public function marshal($value) return ''; } - if(is_object($value)) { + if (is_object($value)) { return ''; } diff --git a/src/Model/Entity/Salesforce.php b/src/Model/Entity/Salesforce.php index 203037f..fec827b 100644 --- a/src/Model/Entity/Salesforce.php +++ b/src/Model/Entity/Salesforce.php @@ -1,4 +1,5 @@ table('Contact'); - $this->displayField('Name'); - $this->primaryKey('Id'); + $this->setTable('Contact'); + $this->setDisplayField('Name'); + $this->setPrimaryKey('Id'); } } diff --git a/src/Model/Table/SalesforcesTable.php b/src/Model/Table/SalesforcesTable.php index 3ea8fbe..51f669e 100644 --- a/src/Model/Table/SalesforcesTable.php +++ b/src/Model/Table/SalesforcesTable.php @@ -1,7 +1,12 @@ table(false); - $this->displayField('Id'); - $this->primaryKey('Id'); + $this->setTable(''); + $this->setDisplayField('Id'); + $this->setPrimaryKey('Id'); - if(!empty($config['connection']->config()['my_wsdl'])) { - $wsdl = CONFIG . DS . $config['connection']->config()['my_wsdl']; + if (!empty($config['connection']->config()['my_wsdl'])) { + $wsdl = CONFIG . DS . $config['connection']->config()['my_wsdl']; } else { - throw new \Exception('You need to provide a WSDL'); + throw new Exception('You need to provide a WSDL'); } $mySforceConnection = new \SforceEnterpriseClient(); $mySforceConnection->createConnection($wsdl); $cache_key = $config['connection']->config()['name'] . '_login'; - $sflogin = (array)Cache::read($cache_key, 'salesforce'); - if(!empty($sflogin['sessionId'])) { - $mySforceConnection->setSessionHeader($sflogin['sessionId']); - $mySforceConnection->setEndpoint($sflogin['serverUrl']); + $sfLogin = (array)Cache::read($cache_key, 'salesforce'); + if (!empty($sfLogin['sessionId'])) { + $mySforceConnection->setSessionHeader($sfLogin['sessionId']); + $mySforceConnection->setEndpoint($sfLogin['serverUrl']); } else { - try{ - $mylogin = $mySforceConnection->login($config['connection']->config()['username'],$config['connection']->config()['password']); - $sflogin = ['sessionId' => $mylogin->sessionId, 'serverUrl' => $mylogin->serverUrl]; - Cache::write($cache_key, $sflogin, 'salesforce'); - } catch (\Exception $e) { + try { + $myLogin = $mySforceConnection->login($config['connection']->config()['username'], + $config['connection']->config()['password']); + $sfLogin = ['sessionId' => $myLogin->sessionId, 'serverUrl' => $myLogin->serverUrl]; + Cache::write($cache_key, $sfLogin, 'salesforce'); + } catch (Exception $e) { $this->log('Error logging into salesforce from Table - Salesforce down?'); throw $e; } } - if (!$sObject = Cache::read($this->name.'_sObject', 'salesforce')) { + if (!$sObject = Cache::read($this->name . '_sObject', 'salesforce')) { $sObject = $mySforceConnection->describeSObject($this->name); - Cache::write($this->name.'_sObject', $sObject, 'salesforce'); + Cache::write($this->name . '_sObject', $sObject, 'salesforce'); } - $this->_fields = Cache::remember($this->name.'_schema', function () use ($sObject) { + $this->_fields = Cache::remember($this->name . '_schema', function () use ($sObject) { $fields = []; foreach ($sObject->fields as $field) { - if(substr($field->soapType,0,3) != 'ens') { //we dont want type of ens - if(substr($field->soapType,4) == 'int') { + if (substr($field->soapType, 0, 3) != 'ens') { //we dont want type of ens + if (substr($field->soapType, 4) == 'int') { $type_name = 'integer'; - } elseif (substr($field->soapType,4) == 'double') { + } elseif (substr($field->soapType, 4) == 'double') { $type_name = 'float'; - } elseif (substr($field->soapType,4) == 'boolean') { + } elseif (substr($field->soapType, 4) == 'boolean') { $type_name = 'boolean'; - } elseif (substr($field->soapType,4) == 'dateTime') { + } elseif (substr($field->soapType, 4) == 'dateTime') { $type_name = 'datetime'; - } elseif (substr($field->soapType,4) == 'date') { + } elseif (substr($field->soapType, 4) == 'date') { $type_name = 'date'; } else { $type_name = 'string'; } - if($field->createable || $field->name == 'Id') { - $fields['creatable'][$field->name] = ['type' => $type_name, 'length' => $field->length, 'null' => $field->nillable]; + if ($field->createable || $field->name == 'Id') { + $fields['creatable'][$field->name] = [ + 'type' => $type_name, + 'length' => $field->length, + 'null' => $field->nillable + ]; } - if($field->updateable || $field->name == 'Id') { - $fields['updatable'][$field->name] = ['type' => $type_name, 'length' => $field->length, 'null' => $field->nillable]; + if ($field->updateable || $field->name == 'Id') { + $fields['updatable'][$field->name] = [ + 'type' => $type_name, + 'length' => $field->length, + 'null' => $field->nillable + ]; } - $fields['selectable'][$field->name] = ['type' => $type_name, 'length' => $field->length, 'null' => $field->nillable]; + $fields['selectable'][$field->name] = [ + 'type' => $type_name, + 'length' => $field->length, + 'null' => $field->nillable + ]; } } @@ -102,53 +121,72 @@ public function initialize(array $config) }, 'salesforce'); //Cache select fields right away as most likely need them immediately - $this->schema($this->_fields['selectable']); + $this->setSchema($this->_fields['selectable']); } /** * {@inheritDoc} */ - public function newEntity($data = null, array $options = []) { - $this->schema($this->_fields['creatable']); + public function newEntity(array $data, array $options = []): EntityInterface + { + $this->setSchema($this->_fields['creatable']); + return parent::newEntity($data, $options); } /** * {@inheritDoc} */ - public function newEntities(array $data, array $options = []) { - $this->schema($this->_fields['creatable']); + public function newEntities(array $data, array $options = []): array + { + $this->setSchema($this->_fields['creatable']); + return parent::newEntities($data, $options); } /** * {@inheritDoc} */ - public function patchEntity(EntityInterface $entity, array $data, array $options = []) { - $this->schema($this->_fields['updatable']); + public function patchEntity(EntityInterface $entity, array $data, array $options = []): EntityInterface + { + $this->setSchema($this->_fields['updatable']); + return parent::patchEntity($entity, $data, $options); } /** * {@inheritDoc} */ - public function patchEntities($entities, array $data, array $options = []) { - $this->schema($this->_fields['updatable']); + public function patchEntities(iterable $entities, array $data, array $options = []): array + { + $this->setSchema($this->_fields['updatable']); + return parent::patchEntities($entities, $data, $options); } - public function beforeFind($event, $query, $options, $primary) { - $this->schema($this->_fields['selectable']); + public function beforeFind(Event $event, Query $query, ArrayObject $options, $primary) + { + $this->setSchema($this->_fields['selectable']); } - public function beforeSave($event, $entity, $options) { - if($options['atomic']) { - throw new \Exception('Salesforce API does not support atomic transactions; set atomic to false.'); + /** + * @param Event $event + * @param EntityInterface $entity + * @param $options + * @return bool + * @throws Exception + */ + public function beforeSave(Event $event, EntityInterface $entity, $options): bool + { + if ($options['atomic']) { + throw new Exception('Salesforce API does not support atomic transactions; set atomic to false.'); } if ($entity->isNew()) { - $this->schema($this->_fields['creatable']); + $this->setSchema($this->_fields['creatable']); } else { - $this->schema($this->_fields['updatable']); + $this->setSchema($this->_fields['updatable']); } + + return true; } } diff --git a/src/ORM/SalesforceMarshaller.php b/src/ORM/SalesforceMarshaller.php index f3e286f..fde32f7 100644 --- a/src/ORM/SalesforceMarshaller.php +++ b/src/ORM/SalesforceMarshaller.php @@ -12,9 +12,11 @@ * @since 3.0.0 * @license http://www.opensource.org/licenses/mit-license.php MIT License */ + namespace Salesforce\ORM; -use ArrayObject; +use Cake\Datasource\EntityInterface; +use Cake\Datasource\InvalidPropertyInterface; use Cake\ORM\Marshaller; use Salesforce\Database\SalesforceType; @@ -30,43 +32,23 @@ */ class SalesforceMarshaller extends Marshaller { + /** - * Hydrate one entity and its associated data. - * - * ### Options: - * - * * associated: Associations listed here will be marshalled as well. - * * fieldList: A whitelist of fields to be assigned to the entity. If not present, - * the accessible fields list in the entity will be used. - * * accessibleFields: A list of fields to allow or deny in entity accessible fields. - * - * The above options can be used in each nested `associated` array. In addition to the above - * options you can also use the `onlyIds` option for HasMany and BelongsToMany associations. - * When true this option restricts the request data to only be read from `_ids`. - * - * ``` - * $result = $marshaller->one($data, [ - * 'associated' => ['Tags' => ['onlyIds' => true]] - * ]); - * ``` - * - * @param array $data The data to hydrate. - * @param array $options List of options - * @return \Cake\ORM\Entity - * @see \Cake\ORM\Table::newEntity() + * {@inheritDoc} */ - public function one(array $data, array $options = []) + public function one(array $data, array $options = []): EntityInterface { list($data, $options) = $this->_prepareDataAndOptions($data, $options); - $primaryKey = (array)$this->_table->primaryKey(); - $entityClass = $this->_table->entityClass(); + $primaryKey = (array)$this->_table->getPrimaryKey(); + $entityClass = $this->_table->getEntityClass(); + /** @var EntityInterface $entity */ $entity = new $entityClass(); - $entity->source($this->_table->registryAlias()); + $entity->setSource($this->_table->getRegistryAlias()); if (isset($options['accessibleFields'])) { foreach ((array)$options['accessibleFields'] as $key => $value) { - $entity->accessible($key, $value); + $entity->setAccess($key, $value); } } $errors = $this->_validate($data, $options, true); @@ -77,7 +59,7 @@ public function one(array $data, array $options = []) foreach ($data as $key => $value) { if (!empty($errors[$key])) { if ($entity instanceof InvalidPropertyInterface) { - $entity->invalid($key, $value); + $entity->setInvalidField($key, $value); } continue; } @@ -88,7 +70,8 @@ public function one(array $data, array $options = []) } elseif (isset($propertyMap[$key])) { $properties[$key] = $propertyMap[$key]($value, $entity); } else { - $columnType = $this->_table->schema()->columnType($key); + $columnType = $this->_table->getSchema() + ->getColumnType($key); if ($columnType) { $converter = SalesforceType::build($columnType); $properties[$key] = $converter->marshal($value); @@ -98,7 +81,7 @@ public function one(array $data, array $options = []) if (!isset($options['fieldList'])) { $entity->set($properties); - $entity->errors($errors); + $entity->setErrors($errors); return $entity; } @@ -109,7 +92,7 @@ public function one(array $data, array $options = []) } } - $entity->errors($errors); + $entity->setErrors($errors); return $entity; } diff --git a/src/ORM/SalesforceQuery.php b/src/ORM/SalesforceQuery.php index dae47c0..18a9bdf 100644 --- a/src/ORM/SalesforceQuery.php +++ b/src/ORM/SalesforceQuery.php @@ -12,9 +12,11 @@ * @since 3.0.0 * @license http://www.opensource.org/licenses/mit-license.php MIT License */ + namespace Salesforce\ORM; use ArrayObject; +use Cake\Datasource\ResultSetInterface; use Salesforce\ORM\SalesforceResultSet; use Salesforce\Database\SalesforceQuery as SalesforceDatabaseQuery; use Cake\ORM\Query; @@ -33,28 +35,10 @@ class SalesforceQuery extends Query { public $queryString = ""; - /** - * Executes this query and returns a ResultSet object containing the results. - * This will also setup the correct statement class in order to eager load deep - * associations. - * - * @return \Cake\ORM\ResultSet - */ - protected function _execute() - { - $this->triggerBeforeFind(); - if ($this->_results) { - $decorator = $this->_decoratorClass(); - return new $decorator($this->_results); - } - $statement = $this->eagerLoader()->loadExternal($this, $this->execute()); - return new SalesforceResultSet($this, $statement); - } - /** * {@inheritDoc} */ - public function sql(ValueBinder $binder = null) + public function sql(?ValueBinder $binder = null): string { $this->triggerBeforeFind(); @@ -67,4 +51,20 @@ public function sql(ValueBinder $binder = null) return $sql; } + + /** + * {@inheritDoc} + */ + protected function _execute(): ResultSetInterface + { + $this->triggerBeforeFind(); + if ($this->_results) { + $decorator = $this->_decoratorClass(); + return new $decorator($this->_results); + } + $statement = $this->getEagerLoader() + ->loadExternal($this, $this->execute()); + + return new SalesforceResultSet($this, $statement); + } } \ No newline at end of file diff --git a/src/ORM/SalesforceResultSet.php b/src/ORM/SalesforceResultSet.php index 434d2a2..9fe046c 100644 --- a/src/ORM/SalesforceResultSet.php +++ b/src/ORM/SalesforceResultSet.php @@ -12,16 +12,12 @@ * @since 3.0.0 * @license http://www.opensource.org/licenses/mit-license.php MIT License */ + namespace Salesforce\ORM; +use Cake\ORM\Query; use Cake\ORM\ResultSet; -use Cake\Collection\Collection; -use Cake\Collection\CollectionTrait; -use Cake\Database\Exception; -use Cake\Database\Type; -use Cake\Datasource\EntityInterface; -use Cake\Datasource\ResultSetInterface; -use SplFixedArray; + /** * Represents the results obtained after executing a query for a specific table @@ -33,12 +29,27 @@ class SalesforceResultSet extends ResultSet { /** - * Creates a map of row keys out of the query select clause that can be - * used to hydrate nested result sets more quickly. - * - * @return void + * {@inheritDoc} + */ + public function first() + { + foreach ($this as $result) { + if ($this->_statement && !$this->_useBuffering) { + $this->_statement->closeCursor(); + } + $result = new $this->_entityClass($result); + $result->clean(); + + return $result; + } + + return null; + } + + /** + * {@inheritDoc} */ - protected function _calculateColumnMap($query) + protected function _calculateColumnMap(Query $query): void { $map = []; //My one foreach ($query->clause('select') as $key => $field) { @@ -63,21 +74,7 @@ protected function _calculateColumnMap($query) } /** - * Correctly nests results keys including those coming from associations - * - * @param mixed $row Array containing columns and values or false if there is no results - * @return array Results - */ - protected function _groupResult($row) - { - return $row; - } - - /** - * Helper function to fetch the next result from the statement or - * seeded results. - * - * @return mixed + * {@inheritDoc} */ protected function _fetchResult() { @@ -93,21 +90,10 @@ protected function _fetchResult() } /** - * Get the first record from a result set. - * - * This method will also close the underlying statement cursor. - * - * @return array|object + * {@inheritDoc} */ - public function first() + protected function _groupResult($row) { - foreach ($this as $result) { - if ($this->_statement && !$this->_useBuffering) { - $this->_statement->closeCursor(); - } - $result = new $this->_entityClass($result); - $result->clean(); - return $result; - } + return $row; } } diff --git a/src/ORM/SalesforceTable.php b/src/ORM/SalesforceTable.php index 2b7d012..0daea53 100644 --- a/src/ORM/SalesforceTable.php +++ b/src/ORM/SalesforceTable.php @@ -12,43 +12,41 @@ * @since 3.0.0 * @license http://www.opensource.org/licenses/mit-license.php MIT License */ + namespace Salesforce\ORM; use \ArrayObject; use Cake\Datasource\EntityInterface; +use Cake\ORM\Marshaller; +use Cake\ORM\Query; use Cake\ORM\Table; -use Salesforce\ORM\SalesforceQuery; -use Salesforce\ORM\SalesforceMarshaller; - class SalesforceTable extends Table { /** * {@inheritDoc} */ - public function query() + public function query(): Query { - return new SalesforceQuery($this->connection(), $this); + return new SalesforceQuery($this->getConnection(), $this); } /** * {@inheritDoc} */ - public function exists($conditions) + public function exists($conditions): bool { - return (bool)count( - $this->find('all') - ->select(['Id']) - ->where($conditions) - ->limit(1) - ->hydrate(false) - ->toArray() - ); + return (bool)count($this->find('all') + ->select(['Id']) + ->where($conditions) + ->limit(1) + ->enableHydration(false) + ->toArray()); } /** - * {@inheritDoc} - */ + * {@inheritDoc} + */ public function save(EntityInterface $entity, $options = []) { $options = new ArrayObject($options + [ @@ -62,15 +60,14 @@ public function save(EntityInterface $entity, $options = []) if (is_array($entity)) { $entity = $this->newEntity($entity); } - if ($entity->errors()) { + if ($entity->getErrors()) { return false; } - if ($entity->isNew() === false && !$entity->dirty()) { + if ($entity->isNew() === false && !$entity->isDirty()) { return $entity; } - $connection = $this->connection(); $success = $this->_processSave($entity, $options); if ($success) { @@ -79,49 +76,46 @@ public function save(EntityInterface $entity, $options = []) } if ($options['atomic'] || $options['_primary']) { - $entity->isNew(false); - $entity->source($this->registryAlias()); + $entity->setNew(false); + $entity->setSource($this->getRegistryAlias()); } } else { - $errors = $this->connection()->driver()->errors[0]; + $errors = $this->getConnection() + ->getDriver()->errors[0]; if (!empty($errors->fields)) { $field = $errors->fields[0]; } else { // For lack of anything better... $field = 'id'; } - $entity->errors($field, $errors->message); + $entity->setErrors($field, $errors->message); } return $success; } + /** - * {@inheritDoc} - */ - public function newEntity($data = null, array $options = []) + * {@inheritDoc} + */ + public function newEntity(array $data, array $options = []): EntityInterface { if ($data === null) { - $class = $this->entityClass(); - $entity = new $class([], ['source' => $this->registryAlias()]); - return $entity; + $class = $this->getEntityClass(); + + return new $class([], ['source' => $this->getRegistryAlias()]); } if (!isset($options['associated'])) { $options['associated'] = $this->_associations->keys(); } $marshaller = $this->marshaller(); + return $marshaller->one($data, $options); } /** - * Get the object used to marshal/convert array data into objects. - * - * Override this method if you want a table object to use custom - * marshalling logic. - * - * @return \Cake\ORM\Marshaller - * @see \Cake\ORM\Marshaller + * {@inheritDoc} */ - public function marshaller() + public function marshaller(): Marshaller { return new SalesforceMarshaller($this); } From 7823aa42c28e1742bc81a9a4ab13272fa3f3b891 Mon Sep 17 00:00:00 2001 From: Yevgeny Tomenko Date: Sun, 1 Aug 2021 18:51:32 +0300 Subject: [PATCH 09/21] implements bulk operations --- composer.json | 1 + src/Model/Table/SalesforcesTable.php | 92 ++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/composer.json b/composer.json index 4f1d50a..8475683 100644 --- a/composer.json +++ b/composer.json @@ -7,6 +7,7 @@ "require": { "php": ">=5.4.16", "cakephp/cakephp": "~3.0", + "ae/salesforce-rest-sdk": "^2.0", "developerforce/force.com-toolkit-for-php": "^1.0@dev" }, "require-dev": { diff --git a/src/Model/Table/SalesforcesTable.php b/src/Model/Table/SalesforcesTable.php index 3ea8fbe..7fa7ca7 100644 --- a/src/Model/Table/SalesforcesTable.php +++ b/src/Model/Table/SalesforcesTable.php @@ -151,4 +151,96 @@ public function beforeSave($event, $entity, $options) { $this->schema($this->_fields['updatable']); } } + + public function toCompositeSObject(EntityInterface $entity, $includeId = false) + { + $object = new CompositeSObject($this->getTable()); + foreach ($entity->extract($entity->getDirty()) as $name => $value) { + $object->{$name} = $value; + } + if ($includeId) { + $object->Id = $entity['Id']; + } + + return $object; + } + + /** + * @param EntityInterface[] $records + * @return CollectionResponse[] + */ + public function createBulk(array $records): array + { + $items = collection($records)->map(function($item) { return $this->toCompositeSObject($item);})->toArray(); + $request = new CollectionRequest($items, true); + + $client = $this->getConnection()->getDriver()->getRestClient(); + $result = $client->getCompositeClient()->create($request); + + foreach ($result as $id => $resultItem) { + if ($resultItem->isSuccess()) { + $records[$id]->set('Id', $resultItem->getId()); + $records[$id]->clean(); + } + } + + return $result; + } + + /** + * @param EntityInterface[] $records + * @return CollectionResponse[] + */ + public function updateBulk(array $records): array + { + $items = collection($records)->map(function($item) { + return $this->toCompositeSObject($item, true); + })->toArray(); + + return $this->getConnection() + ->getDriver() + ->getRestClient() + ->getCompositeClient() + ->update(new CollectionRequest($items, true)); + } + + /** + * @param array $ids + * @return CollectionResponse[] + */ + public function readBulk(array $ids, $fields = null): array + { + $this->schema($this->_fields['selectable']); + if ($fields === null) { + $fields = array_keys($this->_fields['selectable']); + } + $client = $this->getConnection()->getDriver()->getRestClient(); + $response = $client->getCompositeClient()->read($this->getTable(), $ids, $fields); + + return collection($response)->map(function (CompositeSObject $item) { + return $this->marshaller()->one($item->getFields()); + //return $this->newEntity($item->getFields()); + })->toArray(); + } + + /** + * @param array $ids + * @return CollectionResponse[] + */ + public function deleteBulk(array $ids): array + { + $items = collection($ids)->map(function($id) { + $object = new CompositeSObject($this->getTable()); + $object->Id = $id; + + return $object; + })->toArray(); + + return $this->getConnection() + ->getDriver() + ->getRestClient() + ->getCompositeClient() + ->delete(new CollectionRequest($items, true)); + } + } From 3744c5ce16b3f70bf49a0d14a6f76151c857f185 Mon Sep 17 00:00:00 2001 From: Yevgeny Tomenko Date: Sun, 1 Aug 2021 20:42:52 +0300 Subject: [PATCH 10/21] step by step upgrade to 4.x --- config/bootstrap.php | 4 +- .../Dialect/SalesforceDialectTrait.php | 7 +- src/Database/Driver/Salesforce.php | 1 + src/Database/Driver/SalesforceDriverTrait.php | 66 +++++++------------ src/Database/SalesforceQuery.php | 10 +-- .../Statement/SalesforceStatement.php | 12 +++- src/Model/Table/SalesforcesTable.php | 3 + src/ORM/SalesforceQuery.php | 10 ++- 8 files changed, 60 insertions(+), 53 deletions(-) diff --git a/config/bootstrap.php b/config/bootstrap.php index 289be14..4a4d993 100644 --- a/config/bootstrap.php +++ b/config/bootstrap.php @@ -2,11 +2,11 @@ use Cake\Cache\Cache; -Cache::config('salesforce', [ +Cache::setConfig('salesforce', [ 'className' => 'Cake\Cache\Engine\FileEngine', 'duration' => '+1 hours', 'probability' => 100, 'path' => CACHE . 'salesforce' . DS, ]); -?> \ No newline at end of file +?> diff --git a/src/Database/Dialect/SalesforceDialectTrait.php b/src/Database/Dialect/SalesforceDialectTrait.php index 063fbed..69e64b4 100644 --- a/src/Database/Dialect/SalesforceDialectTrait.php +++ b/src/Database/Dialect/SalesforceDialectTrait.php @@ -15,6 +15,7 @@ namespace Salesforce\Database\Dialect; +use Cake\Database\Schema\SchemaDialect; use Salesforce\Database\Schema\SalesforceSchema; use Cake\Database\SqlDialectTrait; @@ -58,7 +59,7 @@ trait SalesforceDialectTrait * * @return \Cake\Database\Schema\MysqlSchema */ - public function schemaDialect() + public function schemaDialect(): SchemaDialect { if (!$this->_schemaDialect) { $this->_schemaDialect = new SalesforceSchema($this); @@ -69,7 +70,7 @@ public function schemaDialect() /** * {@inheritDoc} */ - public function disableForeignKeySQL() + public function disableForeignKeySQL(): string { return ''; } @@ -77,7 +78,7 @@ public function disableForeignKeySQL() /** * {@inheritDoc} */ - public function enableForeignKeySQL() + public function enableForeignKeySQL(): string { return ''; } diff --git a/src/Database/Driver/Salesforce.php b/src/Database/Driver/Salesforce.php index a3b3069..018340e 100644 --- a/src/Database/Driver/Salesforce.php +++ b/src/Database/Driver/Salesforce.php @@ -75,6 +75,7 @@ public function prepare($query): StatementInterface if ($isObject && $query->isBufferedResultsEnabled() === false) { $result->bufferResults(false); } + return $result; } diff --git a/src/Database/Driver/SalesforceDriverTrait.php b/src/Database/Driver/SalesforceDriverTrait.php index 850ee18..00d6dbe 100644 --- a/src/Database/Driver/SalesforceDriverTrait.php +++ b/src/Database/Driver/SalesforceDriverTrait.php @@ -4,7 +4,9 @@ use Cake\Database\Query; use Cake\Cache\Cache; +use Cake\Database\StatementInterface; use Cake\Log\Log; +use PDO; /** * SF driver trait @@ -18,15 +20,13 @@ trait SalesforceDriverTrait */ public $client; + /** + * @var null|\PDO $connection The PDO connection instance + */ protected $_connection; /** - * Returns correct connection resource or object that is internally used - * If first argument is passed, it will set internal connection object or - * result to the value passed - * - * @param null|\PDO $connection The PDO connection instance. - * @return mixed connection object used internally + * {@inheritDoc} */ public function connection($connection = null) { @@ -37,22 +37,17 @@ public function connection($connection = null) } /** - * Disconnects from database server - * - * @return void + * {@inheritDoc} */ - public function disconnect() + public function disconnect(): void { $this->_connection = null; } /** - * Prepares a sql statement to be executed - * - * @param string|\Cake\Database\Query $query The query to turn into a prepared statement. - * @return \Cake\Database\StatementInterface + * {@inheritDoc} */ - public function prepare($query) + public function prepare($query): StatementInterface { $this->connect(); $isObject = $query instanceof Query; @@ -61,11 +56,9 @@ public function prepare($query) } /** - * Starts a transaction - * - * @return bool true on success, false otherwise + * {@inheritDoc} */ - public function beginTransaction() + public function beginTransaction(): bool { $this->connect(); if ($this->_connection->inTransaction()) { @@ -75,53 +68,44 @@ public function beginTransaction() } /** - * Commits a transaction - * - * @return bool true on success, false otherwise + * {@inheritDoc} */ - public function commitTransaction() + public function commitTransaction(): bool { $this->connect(); if (!$this->_connection->inTransaction()) { return false; } + return $this->_connection->commit(); } /** - * Rollsback a transaction - * - * @return bool true on success, false otherwise + * {@inheritDoc} */ - public function rollbackTransaction() + public function rollbackTransaction(): bool { if (!$this->_connection->inTransaction()) { return false; } + return $this->_connection->rollback(); } /** - * Returns a value in a safe representation to be used in a query string - * - * @param mixed $value The value to quote. - * @param string $type Type to be used for determining kind of quoting to perform - * @return string + * {@inheritDoc} */ - public function quote($value, $type) + public function quote($value, $type = PDO::PARAM_STR): string { $this->connect(); + return $this->_connection->quote($value, $type); } /** - * Returns last id generated for a table or sequence in database - * - * @param string|null $table table name or sequence to get last insert value from - * @param string|null $column the name of the column representing the primary key - * @return string|int + * {@inheritDoc} */ - public function lastInsertId($table = null, $column = null) + public function lastInsertId(?string $table = null, ?string $column = null) { $this->connect(); return $this->_connection->lastInsertId($table); @@ -132,7 +116,7 @@ public function lastInsertId($table = null, $column = null) * * @return bool */ - public function supportsQuoting() + public function supportsQuoting(): bool { return false; } @@ -145,7 +129,7 @@ public function supportsQuoting() * @return bool true on success * @throws \ErrorException */ - protected function _connect($dns, array $config) + protected function _connect(string $dsn, array $config): bool { $this->config = $config; diff --git a/src/Database/SalesforceQuery.php b/src/Database/SalesforceQuery.php index 7845e89..304b74b 100644 --- a/src/Database/SalesforceQuery.php +++ b/src/Database/SalesforceQuery.php @@ -22,6 +22,7 @@ use Cake\Database\Expression\ValuesExpression; use Cake\Database\Statement\CallbackStatement; use Cake\Database\ValueBinder; +use Closure; use IteratorAggregate; use RuntimeException; @@ -55,12 +56,13 @@ class SalesforceQuery extends Query * @param array $parts the query clauses to traverse * @return $this */ - public function traverse(callable $visitor, array $parts = []) + public function traverse(Closure $callback) { - $parts = $parts ?: array_keys($this->_parts); - foreach ($parts as $name) { - $visitor($this->_parts[$name], $name); + + foreach ($this->_parts as $name => $part) { + $callback($part, $name); } + return $this; } diff --git a/src/Database/Statement/SalesforceStatement.php b/src/Database/Statement/SalesforceStatement.php index 78443c5..b7b5a24 100644 --- a/src/Database/Statement/SalesforceStatement.php +++ b/src/Database/Statement/SalesforceStatement.php @@ -16,7 +16,9 @@ namespace Salesforce\Database\Statement; use AssignmentRuleHeader; +use Cake\Database\DriverInterface; use Cake\Database\Statement\StatementDecorator; +use Cake\Database\StatementInterface; /** * Statement class meant to be used by a Mysql PDO driver @@ -35,6 +37,12 @@ class SalesforceStatement extends StatementDecorator private $_last_insert_id = []; + public function __construct($statement, DriverInterface $driver) + { + $this->_statement = $statement; + $this->_driver = $driver; + } + /** * {@inheritDoc} * @@ -43,7 +51,7 @@ class SalesforceStatement extends StatementDecorator public function execute(?array $params = null): bool { $sql = $this->_statement->sql(); - $bindings = $this->_statement->valueBinder() + $bindings = $this->_statement->getValueBinder() ->bindings(); $this->_driver->errors = null; @@ -113,7 +121,7 @@ public function execute(?array $params = null): bool $this->last_rows_affected = $result->size; $this->last_result = $result; - return $result; + return true; } /** diff --git a/src/Model/Table/SalesforcesTable.php b/src/Model/Table/SalesforcesTable.php index d165853..c574d4d 100644 --- a/src/Model/Table/SalesforcesTable.php +++ b/src/Model/Table/SalesforcesTable.php @@ -2,6 +2,9 @@ namespace Salesforce\Model\Table; +use AE\SalesforceRestSdk\Model\Rest\Composite\CollectionRequest; +use AE\SalesforceRestSdk\Model\Rest\Composite\CollectionResponse; +use AE\SalesforceRestSdk\Model\Rest\Composite\CompositeSObject; use ArrayObject; use Cake\Datasource\EntityInterface; use Cake\Event\Event; diff --git a/src/ORM/SalesforceQuery.php b/src/ORM/SalesforceQuery.php index 18a9bdf..2590ffa 100644 --- a/src/ORM/SalesforceQuery.php +++ b/src/ORM/SalesforceQuery.php @@ -47,7 +47,15 @@ public function sql(?ValueBinder $binder = null): string // Not a static function, and can't be made static because of base class, // so we @hide the warning about a non-static function used in a static // context. - $sql = @SalesforceDatabaseQuery::sql(null, $this); + + //$sql = SalesforceDatabaseQuery::sql(null, $this); + + $generator = $this->getValueBinder(); + $generator->resetCount(); + + $sql = $this->getConnection() + ->compileQuery($this, $generator); + return $sql; } From 6dbc246cee5f210746c48522cb2fa4e5fda926d6 Mon Sep 17 00:00:00 2001 From: Yevgeny Tomenko Date: Sun, 1 Aug 2021 20:46:17 +0300 Subject: [PATCH 11/21] implements Driver::getRestClient --- src/Database/Driver/Salesforce.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/Database/Driver/Salesforce.php b/src/Database/Driver/Salesforce.php index dae8265..2aa0eba 100644 --- a/src/Database/Driver/Salesforce.php +++ b/src/Database/Driver/Salesforce.php @@ -14,8 +14,11 @@ */ namespace Salesforce\Database\Driver; +use AE\SalesforceRestSdk\AuthProvider\CachedSoapProvider; +use AE\SalesforceRestSdk\Rest\Client; use Cake\Database\Query; use Cake\Database\ValueBinder; +use Cake\Filesystem\File; use Salesforce\Database\Dialect\SalesforceDialectTrait; use Salesforce\Database\Driver\SalesforceDriverTrait; use Salesforce\Database\SalesforceQueryCompiler; @@ -133,4 +136,27 @@ public function compileQuery(Query $query, ValueBinder $generator) $query = $translator($query); return [$query, $processor->compile($query, $generator)]; } + + protected $restClient; + + public function getRestClient() + { + $wsdl = new File(CONFIG . $this->_config['my_wsdl']); + $res = preg_match('#soap:address location="(https://.*)/services/Soap/c/(\d+.\d)#', $wsdl->read(), $matches); + if ($this->restClient === null) { + $this->restClient = new Client( + new CachedSoapProvider( + new FilesystemAdapter('', 0, CACHE), + $this->_config['username'], + $this->_config['password'], + $matches[1] + ), + $matches[2], + ); + + } + + return $this->restClient; + } + } From c548146cae69e6fe9aac7d8151f995ec0099bf9e Mon Sep 17 00:00:00 2001 From: Yevgeny Tomenko Date: Mon, 2 Aug 2021 04:00:07 +0300 Subject: [PATCH 12/21] step by step upgrade to 4.x --- src/Database/Driver/Salesforce.php | 3 ++- src/Database/Statement/SalesforceStatement.php | 5 ++--- src/Model/Table/SalesforcesTable.php | 10 ++++++---- src/ORM/SalesforceTable.php | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Database/Driver/Salesforce.php b/src/Database/Driver/Salesforce.php index 6f79826..0629c8b 100644 --- a/src/Database/Driver/Salesforce.php +++ b/src/Database/Driver/Salesforce.php @@ -28,6 +28,7 @@ use Salesforce\Database\SalesforceQuery; use Salesforce\Database\Statement\SalesforceStatement; use Cake\Database\Driver; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; class Salesforce extends Driver { @@ -155,7 +156,7 @@ public function getRestClient() $this->_config['password'], $matches[1] ), - $matches[2], + $matches[2] ); } diff --git a/src/Database/Statement/SalesforceStatement.php b/src/Database/Statement/SalesforceStatement.php index b7b5a24..237f91a 100644 --- a/src/Database/Statement/SalesforceStatement.php +++ b/src/Database/Statement/SalesforceStatement.php @@ -51,14 +51,13 @@ public function __construct($statement, DriverInterface $driver) public function execute(?array $params = null): bool { $sql = $this->_statement->sql(); - $bindings = $this->_statement->getValueBinder() - ->bindings(); + $bindings = $this->_statement->getValueBinder()->bindings(); $this->_driver->errors = null; //intercept Update here if ($this->_statement->type() == 'update') { $results = $this->_driver->client->update([$this->_buildObjectFromUpdate($sql, $bindings)], - $this->_statement->repository()->name); + $this->_statement->getRepository()->name); if (is_object($results)) { trigger_error('Unexpected object results', E_USER_ERROR); } diff --git a/src/Model/Table/SalesforcesTable.php b/src/Model/Table/SalesforcesTable.php index c574d4d..78b865b 100644 --- a/src/Model/Table/SalesforcesTable.php +++ b/src/Model/Table/SalesforcesTable.php @@ -251,16 +251,18 @@ public function updateBulk(array $records): array */ public function readBulk(array $ids, $fields = null): array { - $this->schema($this->_fields['selectable']); + $this->getSchema($this->_fields['selectable']); if ($fields === null) { $fields = array_keys($this->_fields['selectable']); } - $client = $this->getConnection()->getDriver()->getRestClient(); - $response = $client->getCompositeClient()->read($this->getTable(), $ids, $fields); + $response = $this->getConnection() + ->getDriver() + ->getRestClient() + ->getCompositeClient() + ->read($this->getTable(), $ids, $fields); return collection($response)->map(function (CompositeSObject $item) { return $this->marshaller()->one($item->getFields()); - //return $this->newEntity($item->getFields()); })->toArray(); } diff --git a/src/ORM/SalesforceTable.php b/src/ORM/SalesforceTable.php index 0daea53..38685de 100644 --- a/src/ORM/SalesforceTable.php +++ b/src/ORM/SalesforceTable.php @@ -88,7 +88,7 @@ public function save(EntityInterface $entity, $options = []) // For lack of anything better... $field = 'id'; } - $entity->setErrors($field, $errors->message); + $entity->setError($field, $errors->message); } return $success; From b09d6a15949339b021b1a1576c132a1b3e1fa0f7 Mon Sep 17 00:00:00 2001 From: Yevgeny Tomenko Date: Wed, 11 Aug 2021 13:11:02 +0300 Subject: [PATCH 13/21] fix typo --- src/Database/Schema/SalesforceSchema.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Schema/SalesforceSchema.php b/src/Database/Schema/SalesforceSchema.php index e2f2b01..eca4a24 100644 --- a/src/Database/Schema/SalesforceSchema.php +++ b/src/Database/Schema/SalesforceSchema.php @@ -47,7 +47,7 @@ public function listTables() } /** - * Custom function that queries the datasource for the table list as SQL doesn't accept + * Custom function that queries the datasource for the table list as SOQL doesn't accept * DESCRIBE functionality */ public function describeColumn($tableName, $config) From c9c304539da05303417e6cf944d6bc35d967c580 Mon Sep 17 00:00:00 2001 From: Yevgeny Tomenko Date: Wed, 1 Sep 2021 23:15:51 +0300 Subject: [PATCH 14/21] update composer reqs --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 8475683..7bea0ca 100644 --- a/composer.json +++ b/composer.json @@ -5,8 +5,8 @@ "homepage": "https://github.com/voycey/cakephp-salesforce", "license": "MIT", "require": { - "php": ">=5.4.16", - "cakephp/cakephp": "~3.0", + "php": ">=7.4", + "cakephp/cakephp": "^4.0" "ae/salesforce-rest-sdk": "^2.0", "developerforce/force.com-toolkit-for-php": "^1.0@dev" }, From b910ca7f6f7368758af9d7fc65cbab70b0f19007 Mon Sep 17 00:00:00 2001 From: Yevgeny Tomenko Date: Wed, 1 Sep 2021 23:17:30 +0300 Subject: [PATCH 15/21] fix formating --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 7bea0ca..502e90d 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": ">=7.4", - "cakephp/cakephp": "^4.0" + "cakephp/cakephp": "^4.0", "ae/salesforce-rest-sdk": "^2.0", "developerforce/force.com-toolkit-for-php": "^1.0@dev" }, From f50e1e88cadbdef7ca4e91d466aee4bcf5c8255d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20M=2E=20Gonz=C3=A1lez=20Mart=C3=ADn?= Date: Fri, 15 Oct 2021 17:01:58 +0100 Subject: [PATCH 16/21] update to getRepository --- src/Database/Statement/SalesforceStatement.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Database/Statement/SalesforceStatement.php b/src/Database/Statement/SalesforceStatement.php index 237f91a..42d4f76 100644 --- a/src/Database/Statement/SalesforceStatement.php +++ b/src/Database/Statement/SalesforceStatement.php @@ -75,14 +75,14 @@ public function execute(?array $params = null): bool $header = new AssignmentRuleHeader(null, true); // run the default lead assignment rule $this->_driver->client->setAssignmentRuleHeader($header); } - $results = $this->_driver->client->create([$object], $this->_statement->repository()->name); + $results = $this->_driver->client->create([$object], $this->_statement->getRepository()->name); if (is_object($results)) { trigger_error('Unexpected object results', E_USER_ERROR); } $result = new \stdClass(); if ($results[0]->success) { $result->size = 1; - $this->_last_insert_id[$this->_statement->repository()->name] = $results[0]->id; + $this->_last_insert_id[$this->_statement->getRepository()->name] = $results[0]->id; } else { $result->size = 0; $this->_driver->errors = $results[0]->errors; From dbe56452de1f9ae73fa5e30633e0e1659ddf9c34 Mon Sep 17 00:00:00 2001 From: Yevgeny Tomenko Date: Tue, 9 Nov 2021 22:09:59 +0300 Subject: [PATCH 17/21] fix date marshaling --- src/Database/SalesforceType.php | 9 ++- src/Database/Type/SalesforceDateTimeType.php | 16 +++++ src/Database/Type/SalesforceDateType.php | 29 ++++++++ src/ORM/SalesforceMarshaller.php | 74 ++++++++++++++++++++ 4 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 src/Database/Type/SalesforceDateTimeType.php create mode 100644 src/Database/Type/SalesforceDateType.php diff --git a/src/Database/SalesforceType.php b/src/Database/SalesforceType.php index 1dbf7c0..7ee027b 100644 --- a/src/Database/SalesforceType.php +++ b/src/Database/SalesforceType.php @@ -37,8 +37,8 @@ class SalesforceType extends Type 'biginteger' => 'Cake\Database\Type\IntegerType', 'binary' => 'Cake\Database\Type\BinaryType', 'boolean' => 'Cake\Database\Type\BoolType', - 'date' => 'Cake\Database\Type\DateType', - 'datetime' => 'Cake\Database\Type\DateTimeType', + 'date' => 'Salesforce\Database\Type\SalesforceDateType', + 'datetime' => 'Salesforce\Database\Type\SalesforceDateTimeType', 'decimal' => 'Cake\Database\Type\FloatType', 'float' => 'Cake\Database\Type\FloatType', 'integer' => 'Cake\Database\Type\IntegerType', @@ -81,4 +81,9 @@ public static function build(string $name): TypeInterface return static::$_builtTypes[$name] = new static::$_types[$name]($name); } + + public static function reset(): void + { + static::$_builtTypes = []; + } } diff --git a/src/Database/Type/SalesforceDateTimeType.php b/src/Database/Type/SalesforceDateTimeType.php new file mode 100644 index 0000000..9180bf1 --- /dev/null +++ b/src/Database/Type/SalesforceDateTimeType.php @@ -0,0 +1,16 @@ +_table->getSchema(); + + // Is a concrete column? + foreach (array_keys($data) as $prop) { + $prop = (string)$prop; + $columnType = $schema->getColumnType($prop); + if ($columnType) { + $map[$prop] = function ($value, $entity) use ($columnType) { + return SalesforceType::build($columnType)->marshal($value); + }; + } + } + + // Map associations + if (!isset($options['associated'])) { + $options['associated'] = []; + } + $include = $this->_normalizeAssociations($options['associated']); + foreach ($include as $key => $nested) { + if (is_int($key) && is_scalar($nested)) { + $key = $nested; + $nested = []; + } + // If the key is not a special field like _ids or _joinData + // it is a missing association that we should error on. + if (!$this->_table->hasAssociation($key)) { + if (substr($key, 0, 1) !== '_') { + throw new InvalidArgumentException(sprintf( + 'Cannot marshal data for "%s" association. It is not associated with "%s".', + (string)$key, + $this->_table->getAlias() + )); + } + continue; + } + $assoc = $this->_table->getAssociation($key); + + if (isset($options['forceNew'])) { + $nested['forceNew'] = $options['forceNew']; + } + if (isset($options['isMerge'])) { + $callback = function ($value, $entity) use ($assoc, $nested) { + /** @var \Cake\Datasource\EntityInterface $entity */ + $options = $nested + ['associated' => [], 'association' => $assoc]; + + return $this->_mergeAssociation($entity->get($assoc->getProperty()), $assoc, $value, $options); + }; + } else { + $callback = function ($value, $entity) use ($assoc, $nested) { + $options = $nested + ['associated' => []]; + + return $this->_marshalAssociation($assoc, $value, $options); + }; + } + $map[$assoc->getProperty()] = $callback; + } + + $behaviors = $this->_table->behaviors(); + foreach ($behaviors->loaded() as $name) { + $behavior = $behaviors->get($name); + if ($behavior instanceof PropertyMarshalInterface) { + $map += $behavior->buildMarshalMap($this, $map, $options); + } + } + + return $map; + } } From aef4b945c2b6d37e3bbe948218a76d49c12ff0c9 Mon Sep 17 00:00:00 2001 From: Yevgeny Tomenko Date: Wed, 10 Nov 2021 16:31:38 +0300 Subject: [PATCH 18/21] fix date marshaling --- src/Database/SalesforceType.php | 102 ++++++++++++++++++++++++++++--- src/ORM/SalesforceMarshaller.php | 1 - 2 files changed, 93 insertions(+), 10 deletions(-) diff --git a/src/Database/SalesforceType.php b/src/Database/SalesforceType.php index 7ee027b..3c9af9b 100644 --- a/src/Database/SalesforceType.php +++ b/src/Database/SalesforceType.php @@ -33,7 +33,7 @@ class SalesforceType extends Type * * @var array */ - protected static $_types = [ + protected static $_typesMap = [ 'biginteger' => 'Cake\Database\Type\IntegerType', 'binary' => 'Cake\Database\Type\BinaryType', 'boolean' => 'Cake\Database\Type\BoolType', @@ -64,26 +64,110 @@ class SalesforceType extends Type ], ]; + /** + * Contains a map of type object instances to be reused if needed. + * + * @var \Cake\Database\TypeInterface[] + */ + protected static $_builtSalesforceTypes = []; + /** * {@inheritDoc} */ public static function build(string $name): TypeInterface { - //force rebuild of string type - if ($name != "string") { - if (isset(static::$_builtTypes[$name])) { - return static::$_builtTypes[$name]; + //force rebuild of string and dates type + if (!in_array($name, ['string', 'date', 'datetime'])) { + if (isset(static::$_builtSalesforceTypes[$name])) { + return static::$_builtSalesforceTypes[$name]; } - if (!isset(static::$_types[$name])) { + if (!isset(static::$_typesMap[$name])) { throw new InvalidArgumentException(sprintf('Unknown type "%s"', $name)); } } - return static::$_builtTypes[$name] = new static::$_types[$name]($name); + return static::$_builtSalesforceTypes[$name] = new static::$_typesMap[$name]($name); + } + + + /** + * Returns an arrays with all the mapped type objects, indexed by name. + * + * @return \Cake\Database\TypeInterface[] + */ + public static function buildAll(): array + { + $result = []; + foreach (static::$_typesMap as $name => $type) { + $result[$name] = static::$_builtSalesforceTypes[$name] ?? static::build($name); + } + + return $result; + } + + /** + * Set TypeInterface instance capable of converting a type identified by $name + * + * @param string $name The type identifier you want to set. + * @param \Cake\Database\TypeInterface $instance The type instance you want to set. + * @return void + */ + public static function set(string $name, TypeInterface $instance): void + { + static::$_builtSalesforceTypes[$name] = $instance; + static::$_typesMap[$name] = get_class($instance); + } + + /** + * Registers a new type identifier and maps it to a fully namespaced classname. + * + * @param string $type Name of type to map. + * @param string $className The classname to register. + * @return void + * @psalm-param class-string<\Cake\Database\TypeInterface> $className + */ + public static function map(string $type, string $className): void + { + static::$_typesMap[$type] = $className; + unset(static::$_builtSalesforceTypes[$type]); + } + + /** + * Set type to classname mapping. + * + * @param string[] $map List of types to be mapped. + * @return void + * @psalm-param array> $map + */ + public static function setMap(array $map): void + { + static::$_typesMap = $map; + static::$_builtSalesforceTypes = []; + } + + /** + * Get mapped class name for given type or map array. + * + * @param string|null $type Type name to get mapped class for or null to get map array. + * @return string[]|string|null Configured class name for given $type or map array. + */ + public static function getMap(?string $type = null) + { + if ($type === null) { + return static::$_typesMap; + } + + return static::$_typesMap[$type] ?? null; } - public static function reset(): void + /** + * Clears out all created instances and mapped types classes, useful for testing + * + * @return void + */ + public static function clear(): void { - static::$_builtTypes = []; + static::$_typesMap = []; + static::$_builtSalesforceTypes = []; } } diff --git a/src/ORM/SalesforceMarshaller.php b/src/ORM/SalesforceMarshaller.php index 2eb9e07..e65d0e5 100644 --- a/src/ORM/SalesforceMarshaller.php +++ b/src/ORM/SalesforceMarshaller.php @@ -101,7 +101,6 @@ public function one(array $data, array $options = []): EntityInterface protected function _buildPropertyMap(array $data, array $options): array { - SalesforceType::reset(); $map = []; $schema = $this->_table->getSchema(); From f9b0d1668b26706f20c8176d43c839f2adf9a410 Mon Sep 17 00:00:00 2001 From: Yevgeny Tomenko Date: Fri, 12 Nov 2021 23:01:06 +0300 Subject: [PATCH 19/21] add safe date transforming --- src/Database/Statement/SalesforceStatement.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Database/Statement/SalesforceStatement.php b/src/Database/Statement/SalesforceStatement.php index 42d4f76..ccf86fe 100644 --- a/src/Database/Statement/SalesforceStatement.php +++ b/src/Database/Statement/SalesforceStatement.php @@ -166,7 +166,11 @@ protected function _replacement($binding, $quote = false) return (float)$binding['value']; case 'datetime': case 'date': - $ret = (string)$binding['value']; + if (is_string($binding['value'])) { + $ret = $binding['value']; + } else { + $ret = $binding['value'] ? $binding['value']->toIso8601String() : ''; + } break; case 'string': $ret = trim($binding['value']); From 384df9993f4ce1bf9b6228ed4d80e078ecf36108 Mon Sep 17 00:00:00 2001 From: gregs Date: Tue, 30 Nov 2021 16:46:52 -0500 Subject: [PATCH 20/21] Bulk operation tweaks --- src/Model/Table/SalesforcesTable.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Model/Table/SalesforcesTable.php b/src/Model/Table/SalesforcesTable.php index 78b865b..7c9077e 100644 --- a/src/Model/Table/SalesforcesTable.php +++ b/src/Model/Table/SalesforcesTable.php @@ -197,6 +197,9 @@ public function toCompositeSObject(EntityInterface $entity, $includeId = false) { $object = new CompositeSObject($this->getTable()); foreach ($entity->extract($entity->getDirty()) as $name => $value) { + if (is_object($value) && method_exists($value, 'toIso8601String')) { + $value = $value->toIso8601String(); + } $object->{$name} = $value; } if ($includeId) { @@ -262,7 +265,9 @@ public function readBulk(array $ids, $fields = null): array ->read($this->getTable(), $ids, $fields); return collection($response)->map(function (CompositeSObject $item) { - return $this->marshaller()->one($item->getFields()); + $entity = $this->marshaller()->one($item->getFields()); + $entity->clean(); + return $entity; })->toArray(); } From 8ab5c6957ed948858488757676a8ea1d8c85a2e6 Mon Sep 17 00:00:00 2001 From: Chris Nizzardini Date: Fri, 2 Dec 2022 09:32:08 -0500 Subject: [PATCH 21/21] Check if `$config['connection']` is not empty Check if `$config['connection']` is not empty to resolve bug: ``` Error: [Error] Call to a member function config() on null in src/Model/Table/SalesforcesTable.php on line 52 ``` --- src/Model/Table/SalesforcesTable.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Table/SalesforcesTable.php b/src/Model/Table/SalesforcesTable.php index 7c9077e..79530a7 100644 --- a/src/Model/Table/SalesforcesTable.php +++ b/src/Model/Table/SalesforcesTable.php @@ -49,7 +49,7 @@ public function initialize(array $config): void $this->setDisplayField('Id'); $this->setPrimaryKey('Id'); - if (!empty($config['connection']->config()['my_wsdl'])) { + if (!empty($config['connection']) && !empty($config['connection']->config()['my_wsdl'])) { $wsdl = CONFIG . DS . $config['connection']->config()['my_wsdl']; } else { throw new Exception('You need to provide a WSDL');