from there, it calls the integrations {action}BatchToService or {create}OneToService * * Most integrations will just need to set the $hasBatchUpdate, $hasBatchDelete, $hasBatchCreate flags to signal we can condense it into a single request, * set the formatForService() method if anything deviates from the defaults * and the endpoint for a particular action */ trait SyncTo { use SyncHelpers, UserConnection; /** * Must be defined according to how each service needs it to be * @param int $itemID * @param string $type * @return array * @throws Exception */ abstract protected function formatForService(int $itemID, string $type = 'post'):array; /** * Defaults to an array of formatted items. Can be overridden in child classes * @param array $itemIDs * @param string $type * @return array Either an array of formatted items, or a single formatted item (if there is 1) */ protected function formatItems(array $itemIDs, string $type):array { $items = []; foreach ($itemIDs as $ID) { try { $items[] = $this->formatForService($ID, $type); } catch (Exception $e){ $this->logError('formatItems', 'Could not format Item for service',[ 'itemID' => $ID, 'type' => $type, 'message' => $e->getMessage(), ]); } } return count($items) === 1 ? $items[0] : $items; } protected function determineType(array $data):string|false { $type = false; if (array_key_exists('posts', $data)) { $type = 'post'; } else if (array_key_exists('terms', $data)) { $type = 'term'; } else if (array_key_exists('users', $data)) { $type = 'user'; } return $type; } /*************************************************************** * Item creation ***************************************************************/ public function createBatchToService($data):array { $type = $this->determineType($data); if (!$type) { $this->logError('createBatchToService', 'No expected keys in data', ['data' => $data]); return $this->noCreatedItems([]); } //Check if any of the submitted ids are already created $created = array_filter($data['items'], function($ID) use ($type) { return !empty($this->getServiceItemID($ID, $type)); }); $updated = []; if (!empty($created)) { $updateData = $data; $updateData['items'] = $created; $updated = $this->updateBatchToService($data); //remove any updated items from the original items to process $data['items'] = array_filter($data['items'], function ($ID) use ($created) { return !in_array($ID, $created); }); } $items = $this->formatItems($data['items'], $type); if (empty($items)) { return $this->noCreatedItems($updated); } if ($this->hasBatchCreate) { $response = $this->sendBatchCreate($items); if (!is_wp_error($response)) { $result = $this->processBatchCreateResponse($data, $response); } else { $this->logError('createBatchToService','Batch create failed',[ 'method' => 'createBatchToService', 'item_ids' => $data['items'], 'error' => $response ]); $this->updateItemStatus($data['items'], 'error'); $result = [ 'outcome' => 'failed_permanent', 'result' => 'Could not update items' ]; } } else { $success = $errors = []; foreach ($items as $item) { $itemResult = $this->createOneToService($item); if (!is_wp_error($itemResult)) { $success[] = $itemResult; } else { $errors[] = $itemResult; } } $result = [ 'outcome' => empty($errors) ? 'success' : (empty($success) ? 'failed' : 'partial'), 'result' => [ 'success' => $success, 'errors' => $errors ] ]; } return array_merge($result, $updated); } /** * To be implemented by integration extension. Updates a single item * @param array $item * @return array|WP_Error */ protected function createOneToService(array $item):array|WP_Error { return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own createOneToService'); } /** * Overridden by child classes * @param array $items * @return array|WP_Error */ protected function sendBatchCreate(array $items):array|WP_Error { return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own sendBatchCreate'); } /** * To be implemented by extensions * @param array $data * @param array $response * @return array */ protected function processBatchCreateResponse(array $data, array $response):array { return [ 'outcome' => 'failed', 'result' => [ 'message' => $this->service_name.' should implement processBatchCreateResponse.' ] ]; } /***************************************************************** * ITEM UPDATING *****************************************************************/ public function updateBatchToService(array $data):array { $type = $this->determineType($data); if (!$type) { $this->logError('createBatchToService', 'No expected keys in data', ['data' => $data]); return $this->noUpdatedItems([]); } $newlyCreated = []; if (!$this->canCreateOnUpdate) { $created = array_filter($data['items'], function($ID) use ($type) { return !empty($this->getServiceItemID($ID, $type)); }); //Test to see if we have any that haven't been created yet. //For some services, items may have to be created before they can be updated if (count($created) !== count($data['items'])) { $notCreated = array_filter($data['items'], function($ID) use ($type) { return empty($this->getServiceItemID($ID, $type)); }); $newData = $data; $newData['items'] = $notCreated; $newlyCreated = $this->createBatchToService($newData); } // If we don't have any that are created, just send the noUpdatedItems response, with any newly created items added if (empty($created)) { return $this->noUpdatedItems($newlyCreated); } $data['items'] = $created; } $items = $this->formatItems($data['items'], $type); if (empty($items)) { return $this->noUpdatedItems($newlyCreated); } if ($this->hasBatchUpdate) { $response = $this->sendBatchUpdate($items); if (!is_wp_error($response)) { $result = $this->processBatchUpdateResponse($data, $response); } else { $this->logError('updateBatchToService','Batch update failed',[ 'method' => 'updateBatchToService', 'item_ids' => $data['items'], 'error' => $response ]); $this->updateItemStatus($data['items'], 'error'); $result = [ 'outcome' => 'failed_permanent', 'result' => 'Could not update items' ]; } } else { $success = $errors = []; //Does not have batch update, manually update each one foreach ($items as $item) { $itemResult = $this->updateOneToService($item); if (!is_wp_error($itemResult)) { $success[] = $itemResult; } else { $errors[] = $itemResult; } } $result = [ 'outcome' => empty($errors) ? 'success' : (empty($success) ? 'failed' : 'partial'), 'result' => [ 'success' => $success, 'errors' => $errors ] ]; } return array_merge($result, $newlyCreated); } /** * To be implemented by integration extension. Updates a single item * @param array $item * @return array|WP_Error */ protected function updateOneToService(array $item):array|WP_Error { return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own updateOneToService method'); } /** * Overridden by child classes * @param array $items * @return array|WP_Error */ protected function sendBatchUpdate(array $items):array|WP_Error { return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own sendBatchUpdate'); } /** * To be implemented by extensions * @param array $data * @param array $response * @return array */ protected function processBatchUpdateResponse(array $data, array $response):array { return [ 'outcome' => 'failed', 'result' => [ 'message' => $this->service_name.' should implement processBatchUpdateResponse.' ] ]; } public function deleteOneToService(array $item):array|WP_Error { return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own deleteOneToService'); } public function sendBatchDelete(array $items):array|WP_Error { return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own sendBatchDelete'); } public function deleteBatchToService(array $data):array { $type = $this->determineType($data); if (!$type) { $this->logError('createBatchToService', 'No expected keys in data', ['data' => $data]); return $this->noCreatedItems([]); } //Check if any submitted ids do not have an integration item ID $notCreated = array_filter($data['items'], function($ID) use ($type) { return empty($this->getServiceItemID($ID, $type)); }); if (!empty($notCreated)) { $this->logError('deleteBatchToService','Could not delete items',['items' => $notCreated]); } $created = array_filter($data['items'], function($ID) use ($type) { return !empty($this->getServiceItemID($ID, $type)); }); if ($this->hasBatchDelete) { $result = $this->sendBatchDelete($created); } else { $errors = $success = []; foreach ($created as $item) { try { $success[] = $this->deleteOneToService($item); } catch (Exception $e) { $errors[] = $e->getMessage(); } } $result = [ 'outcome' => empty($errors) ? 'success' : (empty($success) ? 'failed' : 'partial'), 'result' => [ 'success' => $success, 'errors' => $errors ] ]; } return $result; } /***************************************************************** * UTILITY *****************************************************************/ protected function noUpdatedItems(array $created):array { $result = [ 'outcome' => 'success', 'result' => [ 'message' => 'No items to update', 'updated' => [], 'errors' => [], ] ]; if (!empty($created)) { error_log('Result before: '.print_r($result, true)); $result = array_merge($result, $created); error_log('Result after merge: '.print_r($result, true)); } return $result; } protected function noCreatedItems(array $updated):array { $result = [ 'outcome' => 'success', 'result' => [ 'message' => 'No items to create', 'created' => [], 'errors' => [], ] ]; if (!empty($updated)) { $result = array_merge($result, $updated); } return $result; } /**************************************************************** * UTILITY ****************************************************************/ protected function getSyncFields(int $itemID, string $type, array $additionalFields = []):array { $meta = match ($type) { 'post' => Meta::forPost($itemID), 'term' => Meta::forTerm($itemID), 'user' => Meta::forUser($itemID), default => false }; if (!$meta) { return []; } $content = match ($type) { 'post' => get_post_type($itemID), 'term' => get_term($itemID)?->taxonomy, 'user' => jvbUserRole($itemID) }; $registrar = Registrar::getInstance($content); if ($registrar) { $additional = $this->getAdditionalFields($registrar->getIntegration($this->service_name)->getContentType()); $additional = array_combine( array_map(fn($k) => str_starts_with($k, '_'.$this->service_name) ? $k : "_{$this->service_name}_{$k}", array_keys($additional)), $additional ); $additionalFields = array_merge($additionalFields, $additional); } $fields = [ 'share_to_' . $this->service_name, '_keep_synced_' . $this->service_name, "_{$this->service_name}_item_id", "_{$this->service_name}_last_sync", "_{$this->service_name}_shared_at", "_{$this->service_name}_sync_status", "_{$this->service_name}_scheduled_at", ... $additionalFields ]; return $meta->getAll($fields); } }