From 279d6e6576308d9c1f5a7038bbdce9e8054de900 Mon Sep 17 00:00:00 2001 From: Marc Michalsky forumZFD Date: Mon, 25 Jan 2021 17:01:16 +0100 Subject: [PATCH] create TwingleProject.sync --- CRM/TwingleCampaign/BAO/TwingleProject.php | 235 ++--------- api/v3/TwingleProject/Sync.php | 447 +++++++++++++++++++++ twinglecampaign.php | 7 +- 3 files changed, 492 insertions(+), 197 deletions(-) create mode 100644 api/v3/TwingleProject/Sync.php diff --git a/CRM/TwingleCampaign/BAO/TwingleProject.php b/CRM/TwingleCampaign/BAO/TwingleProject.php index 3c17d17..dd0b2f2 100644 --- a/CRM/TwingleCampaign/BAO/TwingleProject.php +++ b/CRM/TwingleCampaign/BAO/TwingleProject.php @@ -29,160 +29,6 @@ class CRM_TwingleCampaign_BAO_TwingleProject extends Campaign { } - /** - * Synchronizes projects between Twingle and CiviCRM (both directions) - * based on the timestamp. - * - * @param array $values - * @param TwingleApiCall $twingleApi - * @param bool $is_test - * If TRUE, don't do any changes - * - * @return array|null - * Returns a response array that contains title, id, project_id and status or - * NULL if $values is not an array - * - * @throws CiviCRM_API3_Exception - */ - public static function sync( - array $values, - TwingleApiCall $twingleApi, - bool $is_test = FALSE - ): ?array { - - // If $values is an array - if (is_array($values)) { - - // Instantiate TwingleProject - try { - $project = new self($values); - } catch (Exception $e) { - $errorMessage = $e->getMessage(); - - // Log Exception - Civi::log()->error( - "Failed to instantiate TwingleProject: $errorMessage" - ); - - // Return result array with error description - return [ - "title" => $values['name'], - "project_id" => (int) $values['id'], - "status" => - "Failed to instantiate TwingleProject: $errorMessage", - ]; - } - - // Check if the TwingleProject campaign already exists - if (!$project->exists()) { - - // ... if not, get embed data and create project - try { - $project->setEmbedData( - $twingleApi->getProjectEmbedData($project->getProjectId()) - ); - $result = $project->create($is_test); - } catch (Exception $e) { - $errorMessage = $e->getMessage(); - - // Log Exception - Civi::log()->error( - "Could not create campaign from TwingleProject: $errorMessage" - ); - - // Return result array with error description - return [ - "title" => $values['name'], - "project_id" => (int) $values['id'], - "status" => - "Could not create campaign from TwingleProject: $errorMessage", - ]; - } - } - else { - $result = $project->getResponse('TwingleProject exists'); - - // If Twingle's version of the project is newer than the CiviCRM - // TwingleProject campaign update the campaign - if ($values['last_update'] > $project->lastUpdate()) { - try { - $project->update($values); - $project->setEmbedData( - $twingleApi->getProjectEmbedData($project->getProjectId()) - ); - $result = $project->create(); - $result['status'] = $result['status'] == 'TwingleProject created' - ? 'TwingleProject updated' - : 'TwingleProject Update failed'; - } catch (Exception $e) { - $errorMessage = $e->getMessage(); - - // Log Exception - Civi::log()->error( - "Could not update TwingleProject campaign: $errorMessage" - ); - // Return result array with error description - $result = $project->getResponse( - "Could not update TwingleProject campaign: $errorMessage" - ); - } - } - // If the CiviCRM TwingleProject campaign was changed, update the project - // on Twingle's side - elseif ($values['last_update'] < $project->lastUpdate()) { - // If this is a test do not make database changes - if ($is_test) { - $result = $project->getResponse( - 'TwingleProject ready to push' - ); - } - else { - $result = $twingleApi->pushProject($project); - // Update TwingleProject in Civi with results from api call - if (is_array($result) && !array_key_exists('message', $result)) { - // Try to update the local TwingleProject campaign - try { - $project->update($result); - $project->create(); - return $project->getResponse('TwingleProject pushed to Twingle'); - } catch (Exception $e) { - // Log Exception - $errorMessage = $e->getMessage(); - Civi::log()->error( - "Could not push TwingleProject campaign: $errorMessage" - ); - // Return result array with error description - return $project->getResponse( - "TwingleProject was likely pushed to Twingle but the local " . - "update of the campaign failed: $errorMessage" - ); - } - } - else { - $message = $result['message']; - return $project->getResponse( - $message - ? "TwingleProject could not get pushed to Twingle: $message" - : 'TwingleProject could not get pushed to Twingle' - ); - } - - } - } - elseif ($result['status'] == 'TwingleProject exists') { - $result = $project->getResponse('TwingleProject up to date'); - } - } - - // Return the result of the synchronization - return $result; - } - else { - return NULL; - } - } - - /** * Export values. Ensures that only those values will be exported which the * Twingle API expects. @@ -217,56 +63,49 @@ class CRM_TwingleCampaign_BAO_TwingleProject extends Campaign { * @param bool $is_test * If true: don't do any changes * - * @return array - * Returns a response array that contains title, id, project_id and status + * @return bool + * Returns a boolean * - * @throws CiviCRM_API3_Exception - * @throws Exception + * @throws \CiviCRM_API3_Exception + * @throws \Exception */ public function create(bool $is_test = FALSE): array { // Create campaign only if this is not a test if (!$is_test) { - // Prepare project values for import into database - $values_prepared_for_import = $this->values; - self::formatValues( - $values_prepared_for_import, - self::IN - ); - self::translateKeys( - $values_prepared_for_import, - self::IN - ); - $this->translateCustomFields( - $values_prepared_for_import, - self::IN - ); + // Prepare project values for import into database + $values_prepared_for_import = $this->values; + self::formatValues( + $values_prepared_for_import, + self::IN + ); + self::translateKeys( + $values_prepared_for_import, + self::IN + ); + $this->translateCustomFields( + $values_prepared_for_import, + self::IN + ); - // Set id - $values_prepared_for_import['id'] = $this->id; + // Set id + $values_prepared_for_import['id'] = $this->id; // Create campaign $result = civicrm_api3('Campaign', 'create', $values_prepared_for_import); - // Update id - $this->id = $result['id']; - - // Check if campaign was created successfully - if ($result['is_error'] == 0) { - $response = $this->getResponse("$this->className created"); - } - else { - $response = $this->getResponse("$this->className creation failed"); - } + // Update id + $this->id = $result['id']; + // Check if campaign was created successfully + if ($result['is_error'] == 0) { + return TRUE; } - // If this is a test, do not create campaign else { - $response = $this->getResponse("$this->className not yet created"); + $errorMessage = $result['error_message']; + throw new Exception("$errorMessage"); } - - return $response; } @@ -394,15 +233,19 @@ class CRM_TwingleCampaign_BAO_TwingleProject extends Campaign { * @return array * Returns a response array that contains title, id, project_id and status */ - public function getResponse(string $status): array { + public function getResponse(string $status = NULL): array { $project_type = empty($this->values['type']) ? 'default' : $this->values['type']; - return [ - 'title' => $this->values['name'], - 'id' => (int) $this->id, - 'project_id' => (int) $this->values['id'], - 'project_type' => $project_type, - 'status' => $status, - ]; + $response = + [ + 'title' => $this->values['name'], + 'id' => (int) $this->id, + 'project_id' => (int) $this->values['id'], + 'project_type' => $project_type, + ]; + if ($status) { + $response['status'] = $status; + } + return $response; } /** diff --git a/api/v3/TwingleProject/Sync.php b/api/v3/TwingleProject/Sync.php new file mode 100644 index 0000000..e3904d5 --- /dev/null +++ b/api/v3/TwingleProject/Sync.php @@ -0,0 +1,447 @@ + 'id', + 'title' => E::ts('Campaign ID'), + 'type' => CRM_Utils_Type::T_INT, + 'api.required' => 0, + 'description' => E::ts('Unique Campaign ID'), + ]; + $spec['project_id'] = [ + 'name' => 'project_id', + 'title' => E::ts('Twingle Project ID'), + 'type' => CRM_Utils_Type::T_INT, + 'api.required' => 0, + 'description' => E::ts('Twingle ID for this project'), + ]; + $spec['is_test'] = [ + 'name' => 'is_test', + 'title' => E::ts('Test'), + 'type' => CRM_Utils_Type::T_BOOLEAN, + 'api.required' => 0, + 'description' => E::ts('If this is set true, no database change will be made'), + ]; + $spec['twingle_api_key'] = [ + 'name' => 'twingle_api_key', + 'title' => E::ts('Twingle API key'), + 'type' => CRM_Utils_Type::T_STRING, + 'api.required' => 0, + 'description' => E::ts('The key to access the Twingle API'), + ]; +} + + +/** + * TwingleProject.Sync API + * + * @param array $params + * + * @return array + * API result descriptor + * @throws \CiviCRM_API3_Exception + * @see civicrm_api3_create_success + */ +function civicrm_api3_twingle_project_Sync(array $params): array { + + // For logging purpose + $extensionName = E::LONG_NAME; + + // If call provides an API key, use it instead of the API key set + // on the extension settings page + $apiKey = empty($params['twingle_api_key']) + ? trim(Civi::settings()->get('twingle_api_key')) + : trim($params['twingle_api_key']); + + // Try to retrieve twingleApi from cache or create a new + $twingleApi = Civi::cache()->get('twinglecampaign_twingle_api'); + if (NULL === $twingleApi || $params['twingle_api_key']) { + try { + $twingleApi = new TwingleApiCall($apiKey); + } catch (Exception $e) { + return civicrm_api3_create_error($e->getMessage()); + } + Civi::cache('long')->set('twinglecampaign_twingle_api', $twingleApi); + } + + // If an id or a project_id is given, synchronize only this one campaign + if ($params['id'] || $params['project_id']) { + + // Get project from db via API + $params['sequential'] = 1; + $result = civicrm_api3('TwingleProject', 'getsingle', $params); + if ($result['is_error'] == 0) { + + // If the TwingleProject campaign already has a project_id try to get the + // project from Twingle + if ($result['values'][0]['project_id']) { + $project_from_twingle = $twingleApi->getProject($result['values'][0]['project_id']); + + // instantiate project from CiviCRM + $id = $result['values'][0]['id']; + unset($result['values'][0]['id']); + $project = new TwingleProject($result['values'][0], $id); + + // Synchronize projects + if (!empty($project_from_twingle)) { + return sync($project, $project_from_twingle, $twingleApi, $params); + } + + // If Twingle does not know a project with the given project_id, give error + else { + return civicrm_api3_create_error( + "The project_id appears to be unknown to Twingle", + $project->getResponse() + ); + } + } + + // If the TwingleProject campaign does not have a project_id, push it to + // Twingle and update it with the returning values + else { + + // store campaign id in $id + $id = $result['values'][0]['id']; + unset($result['values'][0]['id']); + + // instantiate project + $project = new TwingleProject($result['values'][0], $id); + + // Push project to Twingle + return pushToTwingle($project, $twingleApi, $params); + } + } + + // If the project could not get retrieved from TwingleProject.getsingle, + // forward API error message + else { + Civi::log()->error( + "$extensionName could retrieve project from TwingleProject.getsingle", + $result + ); + return $result; + } + } + + // If no id or project_id is given, synchronize all projects + else { + + // Counter for sync errors + $errors_occurred = 0; + + // Get all projects from Twingle + $projects_from_twingle = $twingleApi->getProject(); + + // Get all TwingleProjects from CiviCRM + $projects_from_civicrm = civicrm_api3('TwingleProject', 'get', + ['is_active' => 1,]); + + // If call to TwingleProject.get failed, forward error message + if ($projects_from_civicrm['is_error'] != 0) { + Civi::log()->error( + "$extensionName could retrieve projects from TwingleProject.get", + $projects_from_civicrm + ); + return $projects_from_civicrm; + } + + // Push missing projects to Twingle + $result_values = []; + foreach ($projects_from_civicrm['values'] as $project_from_civicrm) { + if (!in_array($project_from_civicrm['project_id'], + array_column($projects_from_twingle, 'id'))) { + // store campaign id in $id + $id = $project_from_civicrm['id']; + unset($project_from_civicrm['id']); + // instantiate project with values from TwingleProject.Get + $project = new TwingleProject($project_from_civicrm, $id); + // push project to Twingle + $result = pushToTwingle($project, $twingleApi, $params); + if ($result['is_error'] != 0) { + $errors_occurred++; + $result_values[$project->getId()] = + $project->getResponse($result['error_message']); + } + else { + $result_values[$project->getId()] = $result['values']; + } + } + } + + // Create missing projects as campaigns in CiviCRM + foreach ($projects_from_twingle as $project_from_twingle) { + if (!in_array($project_from_twingle['id'], + array_column($projects_from_civicrm['values'], 'project_id'))) { + $project = new TwingleProject($project_from_twingle); + + // If this is a test, do not make db changes + if ($params['is_test']) { + $result_values[$project->getId()] = + $project->getResponse('Ready to create TwingleProject'); + } + + try { + $project->create(TRUE); + $result_values[$project->getId()] = + $project->getResponse('TwingleProject created'); + } catch (Exception $e) { + $errors_occurred++; + $errorMessage = $e->getMessage(); + Civi::log()->error( + "$extensionName could not create TwingleProject: $errorMessage", + $project->getResponse() + ); + $result_values[$project->getId()] = $project->getResponse( + "TwingleProject could not get created: $errorMessage" + ); + } + } + } + + // Synchronize existing projects + foreach ($projects_from_civicrm['values'] as $project_from_civicrm) { + foreach ($projects_from_twingle as $project_from_twingle) { + if ($project_from_twingle['id'] == $project_from_civicrm['project_id']) { + // store campaign id in $id + $id = $project_from_civicrm['id']; + unset($project_from_civicrm['id']); + // instantiate project with values from TwingleProject.Get + $project = new TwingleProject($project_from_civicrm, $id); + // sync project + $result = sync($project, $project_from_twingle, $twingleApi, $params); + if ($result['is_error'] != 0) { + $errors_occurred++; + $result_values[$project->getId()] = + $project->getResponse($result['error_message']); + + } + else { + $result_values[$project->getId()] = $result['values']; + } + break; + } + } + } + + // Give back results + if ($errors_occurred > 0) { + $errorMessage = ($errors_occurred > 1) + ? "$errors_occurred synchronisation processes resulted with an error" + : "1 synchronisation process resulted with an error"; + return civicrm_api3_create_error( + $errorMessage, + $result_values + ); + } + else { + return civicrm_api3_create_success( + $result_values, + $params, + 'TwingleProject', + 'Sync' + ); + } + } +} + + +/** + * Update a TwingleProject campaign locally + * + * @param array $project_from_twingle + * @param \CRM_TwingleCampaign_BAO_TwingleProject $project + * @param array $params + * @param \CRM_TwingleCampaign_BAO_TwingleApiCall $twingleApi + * + * @return array + */ +function updateLocally(array $project_from_twingle, + TwingleProject $project, + array $params, + TwingleApiCall $twingleApi): array { + + // For logging purpose + $extensionName = E::LONG_NAME; + + try { + $project->update($project_from_twingle); + $project->setEmbedData( + $twingleApi->getProjectEmbedData($project->getProjectId()) + ); + // If this is a test, do not make db changes + if ($params['is_test']) { + return civicrm_api3_create_success( + $project->getResponse('TwingleProject ready to update'), + $params, + 'TwingleProject', + 'Sync' + ); + } + // ... else, update local TwingleProject campaign + try { + $project->create(TRUE); + return civicrm_api3_create_success( + $project->getResponse('TwingleProject updated successfully'), + $params, + 'TwingleProject', + 'Sync' + ); + } catch (Exception $e) { + $errorMessage = $e->getMessage(); + Civi::log()->error( + "$extensionName could not update TwingleProject: $errorMessage", + $project->getResponse() + ); + return civicrm_api3_create_error( + "TwingleProject could not get updated: $errorMessage", + $project->getResponse() + ); + } + } catch (Exception $e) { + $errorMessage = $e->getMessage(); + Civi::log()->error( + "$extensionName could not update TwingleProject campaign: $errorMessage" + ); + return civicrm_api3_create_error( + "Could not update TwingleProject campaign: $errorMessage", + $project->getResponse() + ); + } +} + + +/** + * Push a TwingleProject via API to Twingle + * + * @param \CRM_TwingleCampaign_BAO_TwingleProject $project + * @param \CRM_TwingleCampaign_BAO_TwingleApiCall $twingleApi + * @param array $params + * + * @return array + * @throws \CiviCRM_API3_Exception + */ +function pushToTwingle(TwingleProject $project, + TwingleApiCall $twingleApi, + array $params): array { + + // For logging purpose + $extensionName = E::LONG_NAME; + + // If this is a test, do not make db changes + if ($params['is_test']) { + return civicrm_api3_create_success( + $project->getResponse('TwingleProject ready to push to Twingle'), + $params, + 'TwingleProject', + 'Sync' + ); + } + + // Push project to Twingle + try { + $result = $twingleApi->pushProject($project); + } catch (Exception $e) { + $errorMessage = $e->getMessage(); + Civi::log()->error( + "$extensionName could not push TwingleProject to Twingle: $errorMessage", + $project->getResponse() + ); + return civicrm_api3_create_error( + "Could not push TwingleProject to Twingle: $errorMessage", + $project->getResponse() + ); + } + + // Update local campaign with data returning from Twingle + if ($result) { + $project->update($result); + // Get embed data + try { + $project->setEmbedData( + $twingleApi->getProjectEmbedData($project->getProjectId()) + ); + // Create updated campaign + $project->create(TRUE); + return civicrm_api3_create_success( + $project->getResponse('TwingleProject pushed to Twingle'), + $params, + 'TwingleProject', + 'Sync' + ); + } catch (Exception $e) { + $errorMessage = $e->getMessage(); + Civi::log()->error( + "$extensionName pushed TwingleProject to Twingle but local update failed: $errorMessage", + $project->getResponse() + ); + return civicrm_api3_create_error( + "TwingleProject was pushed to Twingle but local update failed: $errorMessage", + $project->getResponse() + ); + } + } + // If the curl fails, the $result may be empty + else { + Civi::log()->error( + "$extensionName could not push TwingleProject campaign", + $project->getResponse() + ); + return civicrm_api3_create_error( + "Could not push TwingleProject campaign", + $project->getResponse() + ); + } +} + + +/** + * Synchronize a TwingleProject campaign with a project from Twingle + * + * @param \CRM_TwingleCampaign_BAO_TwingleProject $project + * @param array $project_from_twingle + * @param \CRM_TwingleCampaign_BAO_TwingleApiCall $twingleApi + * @param array $params + * + * @return array + * @throws \CiviCRM_API3_Exception + */ +function sync(TwingleProject $project, + array $project_from_twingle, + TwingleApiCall $twingleApi, + array $params): array { + + // If Twingle's version of the project is newer than the CiviCRM + // TwingleProject campaign, update the campaign + if ($project_from_twingle['last_update'] > $project->lastUpdate()) { + return updateLocally($project_from_twingle, $project, $params, $twingleApi); + } + + // If the CiviCRM TwingleProject campaign was changed, update the project + // on Twingle's side + elseif ($project_from_twingle['last_update'] < $project->lastUpdate()) { + return pushToTwingle($project, $twingleApi, $params); + } + + // If both versions are still synchronized + else { + return civicrm_api3_create_success( + $project->getResponse('TwingleProject up to date'), + $params, + 'TwingleProject', + 'Sync' + ); + } +} diff --git a/twinglecampaign.php b/twinglecampaign.php index d2237aa..ed55b61 100644 --- a/twinglecampaign.php +++ b/twinglecampaign.php @@ -43,8 +43,13 @@ function twinglecampaign_civicrm_postSave_civicrm_campaign($dao) { } } +/** + * This callback function synchronizes a recently updated TwingleProject campaign + * @param $campaign_id + * @throws \CiviCRM_API3_Exception + */ function twinglecampaign_postSave_callback($campaign_id) { - civicrm_api3('TwingleSync', 'sync', ['id' => $campaign_id]); + civicrm_api3('TwingleProject', 'sync', ['id' => $campaign_id]); } /**