diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/README.md b/README.md index 1ff50c0..53e99a4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ # Vtiger (Laravel 5 Package) -Use the Vtiger webservice (REST) API from within Laravel for operations Create, Retrieve and Update. +Use the Vtiger webservice (REST) API from within Laravel for the following operations. + +- Create +- Retrieve +- Update +- Delete +- Query +- Describe See [Third Party App Integration (REST APIs)](http://community.vtiger.com/help/vtigercrm/developers/third-party-app-integration.html) @@ -12,7 +19,7 @@ See [Third Party App Integration (REST APIs)](http://community.vtiger.com/help/v composer require "clystnet/vtiger ~1.3" ``` - > *If you are using Laravel 5.5 you don’t need to do steps 2 and 3.* + > *If you are using Laravel >= 5.5 you don’t need to do steps 2 and 3.* 2. Then in your config/app.php add the following to the providers array: @@ -36,13 +43,19 @@ See [Third Party App Integration (REST APIs)](http://community.vtiger.com/help/v - In Vtiger, create a new or select an existing user. - Under *User Advanced Options* make note of the *username* and *access key*. -- In your application, edit *config/vtiger.php* and replace the following array values with your CRM username and access key. Also set the url to the webservice.php - - |key |value | - |---------|-------------------------------------| - |url |http://www.example.com/webservice.php| - |username |API | - |accesskey|irGsy9HB0YOZdEA | +- In your application, edit *config/vtiger.php* and replace the following array values + - Set the url to the https://{DOMAIN_NAME}/webservice.php + - Set the username and accesskey with your CRM username and access key. + - Set the session drive to either file or reddis + - Set persistconnection to false if you want a fresh login with each request + + |key |value | + |-----------------|-------------------------------------| + |url |http://www.example.com/webservice.php| + |username |API | + |accesskey |irGsy9HB0YOZdEA | + |sessiondriver |file | + |persistconnection|true | > Because I've experienced problems getting the sessionid from the CRM when multiple users are accessing the CRM at the same time, the solution was to store the sessionid into a file within Laravel application. > Instead of getting the token from the database for each request using the webservice API, a check is made against the expiry time in the file. If the expiry time has expired, a token is requested from the CRM and file is updated with the new token and updated expiry time. @@ -50,28 +63,28 @@ See [Third Party App Integration (REST APIs)](http://community.vtiger.com/help/v ### Usage In your controller include the Vtiger package -``` +```php use Vtiger; ``` #### Create To insert a record into the CRM, first create an array of data to insert. Don't forget the added the id of the `assigned_user_id` (i.e. '4x12') otherwise the insert will fail as `assigned_user_id` is a mandatory field. -``` +```php $data = array( 'assigned_user_id' => '', ); ``` To do the actual insert, pass the module name along with the json encoded array to the *create* function. -``` +```php Vtiger::create($MODULE_NAME, json_encode($data)); ``` #### Retrieve To retrieve a record from the CRM, you need the id of the record you want to find (i.e. '4x12'). -``` +```php $id = '4x12'; $obj = Vtiger::retrieve($id); @@ -83,28 +96,40 @@ var_dump($obj); #### Update The easiest way to update a record in the CRM is to retrieve the record first. -``` +```php $id = '4x12'; $obj = Vtiger::retrieve($id); ``` Then update the object with updated data. -``` +```php $obj->result->field_name = 'Your new value'; $update = Vtiger::update($obj->result); ``` +#### Delete + +To delete a record from the CRM, you need the id of the record you want to delete (i.e. '4x12'). +```php +$id = '4x12'; + +$obj = Vtiger::retrieve($id); + +// do someting with the result +var_dump($obj); +``` + #### Query To use the [Query Operation](http://community.vtiger.com/help/vtigercrm/developers/third-party-app-integration.html#query-operation), you first need to create a SQL query. -``` +```php $query = "SELECT * FROM ModuleName;"; ``` Then run the query... -``` +```php $obj = Vtiger::query($query); //loop over result @@ -113,6 +138,14 @@ foreach($obj->result as $result) { } ``` +### Describe + +To describe modules in the CRM run this with the module name + +```php +$moduleDescription = (Vtiger:describe("Contacts"))->result; +``` + ## Contributing Please report any issue you find in the issues page. Pull requests are more than welcome. @@ -120,6 +153,7 @@ Please report any issue you find in the issues page. Pull requests are more than ## Authors * **Adam Godfrey** - [Clystnet](https://www.clystnet.com) +* **Chris Pratt** - [Clystnet](https://www.clystnet.com) ## License diff --git a/src/Config/config.php b/src/Config/config.php index efa71d0..eb1cf2e 100644 --- a/src/Config/config.php +++ b/src/Config/config.php @@ -2,7 +2,7 @@ // vTiger API constants return [ - 'url' => 'path/to/vtiger/webservice', + 'url' => 'path/to/vtiger/webservice.php', 'username' => '', 'accesskey' => '', 'sessiondriver' => 'file', //reddis or file diff --git a/src/Facades/Vtiger.php b/src/Facades/Vtiger.php index b0b8232..d482436 100644 --- a/src/Facades/Vtiger.php +++ b/src/Facades/Vtiger.php @@ -6,8 +6,10 @@ class Vtiger extends Facade { + protected static function getFacadeAccessor() { return 'clystnet-vtiger'; } + } \ No newline at end of file diff --git a/src/Vtiger.php b/src/Vtiger.php index d8fb750..b16b948 100644 --- a/src/Vtiger.php +++ b/src/Vtiger.php @@ -4,21 +4,29 @@ use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Client; use Mockery\CountValidator\Exception; +use Psr\Http\Message\ResponseInterface; use Storage; use Config; use Redis; +/** + * Laravel wrapper for the VTgier API + * + * Class Vtiger + * @package Clystnet\Vtiger + */ class Vtiger { - protected $url; - protected $username; - protected $accesskey; - protected $sessionDriver; - protected $persistConnection; + + /** @var string */ + protected $url, $username, $accesskey, $sessionDriver, $persistConnection; + + /** @var Client */ protected $client; - protected $retry; - protected $maxretry; + /** + * Vtiger constructor. + */ public function __construct() { // set the API url and username @@ -29,12 +37,17 @@ public function __construct() $this->persistConnection = Config::get('vtiger.persistconnection'); $this->client = new Client(['http_errors' => false, 'verify' => false]); //GuzzleHttp\Client - - $this->retry = true; - $this->maxretry = Config::get('vtiger.maxretry'); } - // user can pass other connection parameters to override the constructor + /** + * Call this function if you wish to override the default connection + * + * @param string $url + * @param string $username + * @param string $accesskey + * + * @return $this + */ public function connection($url, $username, $accesskey) { $this->url = $url; @@ -44,156 +57,196 @@ public function connection($url, $username, $accesskey) return $this; } + /** + * Get the session id for a login either from a stored session id or fresh from the API + * + * @return string + * @throws GuzzleException + * @throws VtigerError + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + */ protected function sessionid() { - // session file exists - if ($this->sessionDriver == 'file') { - if (Storage::disk('local')->exists('session.json')) { - $json = json_decode(Storage::disk('local')->get('session.json')); - } - } elseif ($this->sessionDriver == 'redis') { - $json = json_decode(Redis::get('clystnet_vtiger')); + + // Check the session file exists + switch ($this->sessionDriver) { + case "file": + if (Storage::disk('local')->exists('session.json')) { + $sessionData = json_decode(Storage::disk('local')->get('session.json')); + } + break; + case "redis": + $sessionData = json_decode(Redis::get('clystnet_vtiger')); + break; + default: + throw new VtigerError("Session driver type of ".$this->sessionDriver." is not supported", 4); } - if ($json) { - if (isset($json) && property_exists($json, 'expireTime') && property_exists($json, 'token')) { - if ($json->expireTime < time() || empty($json->token)) { - $json = $this->storesession(); + if (isset($sessionData)) { + if (isset($sessionData) && property_exists($sessionData, 'expireTime') && property_exists($sessionData, 'token')) { + if ($sessionData->expireTime < time() || empty($sessionData->token)) { + $sessionData = $this->storesession(); } } else { - $json = $this->storesession(); + $sessionData = $this->storesession(); } } else { - $json = $this->storesession(); + $sessionData = $this->storesession(); } if (isset($json->sessionid)) { $sessionid = $json->sessionid; } else { - $sessionid = self::login($json); + $sessionid = $this->login($sessionData); } return $sessionid; + } - public function login($json) + /** + * Login to the VTiger API to get a new session + * + * @param object $sessionData + * + * @return string + * @throws GuzzleException + * @throws VtigerError + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + */ + protected function login($sessionData) { - $token = $json->token; + + $token = $sessionData->token; // Create unique key using combination of challengetoken and accesskey $generatedkey = md5($token . $this->accesskey); - $tries = 0; - $keep = true; - - while ($keep && $tries < $this->maxretry) { - // login using username and accesskey - $response = $this->client->request('POST', $this->url, [ - 'form_params' => [ - 'operation' => 'login', - 'username' => $this->username, - 'accessKey' => $generatedkey - ] - ]); - Redis::incr('loggedin'); - // decode the response - $login_result = json_decode($response->getBody()->getContents()); - - // If api login failed - if ($response->getStatusCode() !== 200 || !$login_result->success) { - $keep = $this->retry; - - if (!$login_result->success && $keep) { - if ($login_result->error->code == "INVALID_USER_CREDENTIALS" || $login_result->error->code == "INVALID_SESSIONID") { - $tries++; - if ($this->sessionDriver == 'file') { - if (Storage::disk('local')->exists('session.json')) { - Storage::disk('local')->delete('session.json'); - } - } elseif ($this->sessionDriver == 'redis') { - Redis::del('clystnet_vtiger'); + // login using username and accesskey + $response = $this->client->request('POST', $this->url, [ + 'form_params' => [ + 'operation' => 'login', + 'username' => $this->username, + 'accessKey' => $generatedkey + ] + ]); + + // decode the response + $loginResult = json_decode($response->getBody()->getContents()); + + // If api login failed + if ($response->getStatusCode() !== 200 || !$loginResult->success) { + if (!$loginResult->success) { + if ($loginResult->error->code == "INVALID_USER_CREDENTIALS" || $loginResult->error->code == "INVALID_SESSIONID") { + if ($this->sessionDriver == 'file') { + if (Storage::disk('local')->exists('session.json')) { + Storage::disk('local')->delete('session.json'); } - continue; + } elseif ($this->sessionDriver == 'redis') { + Redis::del('clystnet_vtiger'); } - } - - if ($response->me) { - return json_encode(array( - 'success' => false, - 'message' => $login_result->error->message - )); + } else { + $this->_processResult($response); } } else { - // login ok so get sessionid and update our session - $sessionid = $login_result->result->sessionName; - if ($this->sessionDriver == 'file') { + $this->_checkResponseStatusCode($response); + } + } else { + // login ok so get sessionid and update our session + $sessionid = $loginResult->result->sessionName; + + switch ($this->sessionDriver) { + case "file": if (Storage::disk('local')->exists('session.json')) { $json = json_decode(Storage::disk('local')->get('session.json')); $json->sessionid = $sessionid; Storage::disk('local')->put('session.json', json_encode($json)); } - } elseif ($this->sessionDriver == 'redis') { + break; + case "redis": + Redis::incr('loggedin'); $json = json_decode(Redis::get('clystnet_vtiger')); $json->sessionid = $sessionid; Redis::set('clystnet_vtiger', json_encode($json)); - } - break; + break; + default: + throw new VtigerError("Session driver type of ".$this->sessionDriver." is not supported", 4); } } return $sessionid; + } + /** + * Store a new session if needed + * + * @return object + * @throws GuzzleException + */ protected function storesession() { - $updated = self::gettoken(); - $json = (object)$updated; + $updated = $this->gettoken(); + + $output = (object)$updated; if ($this->sessionDriver == 'file') { - Storage::disk('local')->put('session.json', json_encode($json)); + Storage::disk('local')->put('session.json', json_encode($output)); } elseif ($this->sessionDriver == 'redis') { - Redis::set('clystnet_vtiger', json_encode($json)); + Redis::set('clystnet_vtiger', json_encode($output)); } - return $json; + return $output; + } + /** + * Get a new access token from the VTiger API + * + * @return array + * @throws GuzzleException + * @throws VtigerError + */ protected function gettoken() { - $bool = false; - - do { - // perform API GET request - $response = $this->client->request('GET', $this->url, [ - 'query' => [ - 'operation' => 'getchallenge', - 'username' => $this->username - ] - ]); - - // decode the response - $challenge = json_decode($response->getBody()); - - // If challenge failed - if ($response->getStatusCode() === 200 || $challenge->success) { - $bool = true; - } - } while (!$bool); + + // perform API GET request + $response = $this->client->request('GET', $this->url, [ + 'query' => [ + 'operation' => 'getchallenge', + 'username' => $this->username + ] + ]); + + // decode the response + $challenge = $this->_processResult($response); // Everything ok so create a token from response - $json = array( + $output = array( 'token' => $challenge->result->token, 'expireTime' => $challenge->result->expireTime, ); - return $json; + return $output; + } + /** + * Logout from the VTiger API + * + * @param string $sessionid + * + * @return object + * @throws GuzzleException + * @throws VtigerError + */ protected function close($sessionid) { + if ($this->persistConnection) { return true; } + // send a request to close current connection $response = $this->client->request('GET', $this->url, [ 'query' => [ @@ -202,171 +255,266 @@ protected function close($sessionid) ] ]); - // decode the response - $data = json_decode($response->getBody()->getContents()); + return $this->_processResult($response); - return $data; } + /** + * Query the VTiger API with the given query string + * + * @param string $query + * + * @return object + * @throws GuzzleException + * @throws VtigerError + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + */ public function query($query) { - $sessionid = self::sessionid(); - if (isset($sessionid->success)) { - return $sessionid->message; - } + $sessionid = self::sessionid(); - for ($i = 0; (!isset($data->success) && $i < 10); $i++) { - // send a request using a database query to get back any matching records - $response = $this->client->request('GET', $this->url, [ - 'query' => [ - 'operation' => 'query', - 'sessionName' => $sessionid, - 'query' => $query - ] - ]); - - // decode the response - $data = json_decode($response->getBody()->getContents()); - } + // send a request using a database query to get back any matching records + $response = $this->client->request('GET', $this->url, [ + 'query' => [ + 'operation' => 'query', + 'sessionName' => $sessionid, + 'query' => $query + ] + ]); self::close($sessionid); - return (isset($data->success)) ? $data : false; + return $this->_processResult($response); + } + /** + * Retreive a record from the VTiger API + * Format of id must be {moudler_code}x{item_id}, e.g 4x12 + * + * @param string $id + * + * @return object + * @throws GuzzleException + * @throws VtigerError + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + */ public function retrieve($id) { - $sessionid = self::sessionid(); - if (isset($sessionid->success)) { - return $sessionid->message; - } + $sessionid = self::sessionid(); - for ($i = 0; (!isset($data->success) && $i < 10); $i++) { - // send a request to retrieve a record - $response = $this->client->request('GET', $this->url, [ - 'query' => [ - 'operation' => 'retrieve', - 'sessionName' => $sessionid, - 'id' => $id - ] - ]); - - // decode the response - $data = json_decode($response->getBody()->getContents()); - } + // send a request to retrieve a record + $response = $this->client->request('GET', $this->url, [ + 'query' => [ + 'operation' => 'retrieve', + 'sessionName' => $sessionid, + 'id' => $id + ] + ]); self::close($sessionid); - return (isset($data->success)) ? $data : false; + return $this->_processResult($response); + } - public function create($elem, $data) + /** + * Create a new entry in the VTiger API + * + * To insert a record into the CRM, first create an array of data to insert. + * Don't forget the added the id of the `assigned_user_id` (i.e. '4x12') otherwise the insert will fail + * as `assigned_user_id` is a mandatory field. + * + * $data = array( + * 'assigned_user_id' => '', + * ); + * + * To do the actual insert, pass the module name along with the json encoded array. + * + * @param string $elem + * @param string $data + * + * @return object + * @throws GuzzleException + * @throws VtigerError + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + */ + public function create(string $elem, string $data) { - $sessionid = self::sessionid(); - if (isset($sessionid->success)) { - return $sessionid->message; - } + $sessionid = self::sessionid(); - for ($i = 0; (!isset($data->success) && $i < 10); $i++) { - // send a request to create a record - $response = $this->client->request('POST', $this->url, [ - 'form_params' => [ - 'operation' => 'create', - 'sessionName' => $sessionid, - 'element' => $data, - 'elementType' => $elem - ] - ]); - - // decode the response - $data = json_decode($response->getBody()->getContents()); - } + // send a request to create a record + $response = $this->client->request('POST', $this->url, [ + 'form_params' => [ + 'operation' => 'create', + 'sessionName' => $sessionid, + 'element' => $data, + 'elementType' => $elem + ] + ]); self::close($sessionid); - return (isset($data->success)) ? $data : false; + return $this->_processResult($response); + } + /** + * Update an entry in the database from the given object + * + * The object should be an object retreived from the database and then altered + * + * @param $object + * + * @return object + * @throws GuzzleException + * @throws VtigerError + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + */ public function update($object) { - $sessionid = self::sessionid(); - if (isset($sessionid->success)) { - return $sessionid->message; - } + $sessionid = self::sessionid(); - for ($i = 0; (!isset($data->success) && $i < 10); $i++) { - // send a request to update a record - $response = $this->client->request('POST', $this->url, [ - 'form_params' => [ - 'operation' => 'update', - 'sessionName' => $sessionid, - 'element' => json_encode($object), - ] - ]); - - // decode the response - $data = json_decode($response->getBody()->getContents()); - } + // send a request to update a record + $response = $this->client->request('POST', $this->url, [ + 'form_params' => [ + 'operation' => 'update', + 'sessionName' => $sessionid, + 'element' => json_encode($object), + ] + ]); self::close($sessionid); - return (isset($data->success)) ? $data : false; + return $this->_processResult($response); + } + /** + * Delete from the database using the given id + * Format of id must be {moudler_code}x{item_id}, e.g 4x12 + * + * @param $id + * + * @return object + * @throws GuzzleException + * @throws VtigerError + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + */ public function delete($id) { - $sessionid = self::sessionid(); - if (isset($sessionid->success)) { - return $sessionid->message; - } + $sessionid = self::sessionid(); - for ($i = 0; (!isset($data->success) && $i < 10); $i++) { - // send a request to delete a record - $response = $this->client->request('GET', $this->url, [ - 'query' => [ - 'operation' => 'delete', - 'sessionName' => $sessionid, - 'id' => $id - ] - ]); - - // decode the response - $data = json_decode($response->getBody()->getContents()); - } + // send a request to delete a record + $response = $this->client->request('GET', $this->url, [ + 'query' => [ + 'operation' => 'delete', + 'sessionName' => $sessionid, + 'id' => $id + ] + ]); self::close($sessionid); - return (isset($data->success)) ? $data : false; + return $this->_processResult($response); + } + /** + * Describe an element from the vTiger API from the given element name + * + * @param string $elementType + * + * @return object + * @throws GuzzleException + * @throws VtigerError + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + */ public function describe($elementType) { + $sessionid = self::sessionid(); - if (isset($sessionid->success)) { - return $sessionid->message; + // send a request to describe a module (which returns a list of available fields) for a Vtiger module + $response = $this->client->request('GET', $this->url, [ + 'query' => [ + 'operation' => 'describe', + 'sessionName' => $sessionid, + 'elementType' => $elementType + ] + ]); + + self::close($sessionid); + + return $this->_processResult($response); + + } + + /** + * Process the response from the API for errors + * + * @param mixed|ResponseInterface $response + * + * @return object + * @throws VtigerError + */ + protected function _processResult($response) + { + + $this->_checkResponseStatusCode($response); + + // decode the response + $data = json_decode($response->getBody()->getContents()); + + if (!isset($data->success)) { + throw new VtigerError("Success property not set on VTiger response", 2); } - for ($i = 0; (!isset($data->success) && $i < 10); $i++) { - // send a request to describe a module (which returns a list of available fields) for a Vtiger module - $response = $this->client->request('GET', $this->url, [ - 'query' => [ - 'operation' => 'describe', - 'sessionName' => $sessionid, - 'elementType' => $elementType - ] - ]); - // decode the response - $data = json_decode($response->getBody()->getContents()); + if ($data->success == false) { + $this->_processResponseError($data); } - self::close($sessionid); + return $data; - return (isset($data->success)) ? $data : false; } + + /** + * Check the response code to make sure it isn't anything but 200 + * + * @param mixed|ResponseInterface $response + * + * @throws VtigerError + */ + protected function _checkResponseStatusCode($response) + { + + if ($response->getStatusCode() !== 200) { + throw new VtigerError("API request did not complete correctley - Response code: ".$response->getStatusCode(), 1); + } + + } + + /** + * Process any errors that we have got back + * + * @param object $processedData + * + * @throws VtigerError + */ + protected function _processResponseError($processedData) + { + + if (!isset($processedData->error)) { + throw new VtigerError("Error property not set on VTiger response when success is false", 3); + } + + throw new VtigerError($processedData->error->message, 4, $processedData->error->code); + + } + } diff --git a/src/VtigerError.php b/src/VtigerError.php new file mode 100644 index 0000000..8508f6f --- /dev/null +++ b/src/VtigerError.php @@ -0,0 +1,33 @@ +vTigerErrorCode = $vTigerErrorCode; + + } + + /** + * @return string + */ + public function getVTigerErrorCode() { + return $this->vTigerErrorCode; + } + +} \ No newline at end of file diff --git a/src/VtigerServiceProvider.php b/src/VtigerServiceProvider.php index 5e669f9..371d6ef 100644 --- a/src/VtigerServiceProvider.php +++ b/src/VtigerServiceProvider.php @@ -6,6 +6,7 @@ class VtigerServiceProvider extends ServiceProvider { + /** * Bootstrap the application services. * @@ -13,6 +14,7 @@ class VtigerServiceProvider extends ServiceProvider */ public function boot() { + $this->publishes([ __DIR__ . '/Config/config.php' => config_path('vtiger.php'), ], 'vtiger'); @@ -21,6 +23,7 @@ public function boot() $this->mergeConfigFrom( __DIR__ . '/Config/config.php', 'vtiger' ); + } /** @@ -30,6 +33,7 @@ public function boot() */ public function register() { + $this->app->bind('clystnet-vtiger', function () { return new Vtiger(); }); @@ -37,5 +41,7 @@ public function register() config([ 'config/vtiger.php', ]); + } + }