Jake Vanderwerf
5 days ago 0dfe1d8afafc59c4a5559c498342668d5a58d6ef
inc/integrations/SyncTo.php
@@ -2,14 +2,34 @@
namespace JVBase\integrations;
use Exception;
use JVBase\meta\Meta;
use JVBase\meta\Sanitizer;
use JVBase\registrar\Registrar;
use WP_Error;
use WP_Post;
use WP_Term;
use WP_User;
if (!defined('ABSPATH')) {
   exit;
}
/**
 * For Syncing to a service, the basic flow is:
 * 1) Queuing an operation
 *       a) using the $syncTo as the operation type
 *       b) the $data has:
 *          i) key of 'posts', 'terms', 'users' depending on the source data
 *          ii) key of 'user' to set user's specific integration connection, or the site-wide connection
 * 2) IntegrationExecutor then determines if it is a sync to operation
 *    -> 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;
   use SyncHelpers, UserConnection;
   /**
    * Must be defined according to how each service needs it to be
    * @param int $itemID
@@ -17,16 +37,13 @@
    * @return array
    * @throws Exception
    */
   protected function formatForService(int $itemID, string $type = 'post'):array
   {
      throw new Exception('formatForService must be implemented by child class');
   }
   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
    * @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
   {
@@ -35,21 +52,39 @@
         try {
            $items[] = $this->formatForService($ID, $type);
         } catch (Exception $e){
            $this->logError('Could not format Item for service',[
            $this->logError('formatItems', 'Could not format Item for service',[
               'itemID' => $ID,
               'type'   => $type,
               'message'   => $e->getMessage(),
            ]);
         }
      }
      return $items;
      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 = $data['type']??'post';
      $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'],
@@ -74,18 +109,12 @@
         return $this->noCreatedItems($updated);
      }
      $response = [
         'outcome'   => 'failed',
         'result' => [
            'message'   => 'No result'
         ]
      ];
      if ($this->hasBatchCreate) {
         $response = $this->sendBatchCreate($items);
         if (!is_wp_error($response)) {
            $result = $this->processBatchCreateResponse($data, $response);
         } else {
            $this->logError('Batch create failed',[
            $this->logError('createBatchToService','Batch create failed',[
               'method' => 'createBatchToService',
               'item_ids'  => $data['items'],
               'error'     => $response
@@ -100,7 +129,7 @@
      } else {
         $success = $errors = [];
         foreach ($items as $item) {
            $itemResult = $this->createOne($item);
            $itemResult = $this->createOneToService($item);
            if (!is_wp_error($itemResult)) {
               $success[] = $itemResult;
            } else {
@@ -124,9 +153,9 @@
    * @param array $item
    * @return array|WP_Error
    */
   protected function createOne(array $item):array|WP_Error
   protected function createOneToService(array $item):array|WP_Error
   {
      return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own createOne');
      return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own createOneToService');
   }
   /**
@@ -160,7 +189,12 @@
   *****************************************************************/
   public function updateBatchToService(array $data):array
   {
      $type = $data['type']??'post';
      $type = $this->determineType($data);
      if (!$type) {
         $this->logError('createBatchToService', 'No expected keys in data', ['data' => $data]);
         return $this->noUpdatedItems([]);
      }
      $newlyCreated = [];
      if (!$this->canCreateOnUpdate) {
@@ -198,7 +232,7 @@
         if (!is_wp_error($response)) {
            $result = $this->processBatchUpdateResponse($data, $response);
         } else {
            $this->logError('Batch update failed',[
            $this->logError('updateBatchToService','Batch update failed',[
               'method' => 'updateBatchToService',
               'item_ids'  => $data['items'],
               'error'     => $response
@@ -214,7 +248,7 @@
         $success = $errors = [];
         //Does not have batch update, manually update each one
         foreach ($items as $item) {
            $itemResult = $this->updateOne($item);
            $itemResult = $this->updateOneToService($item);
            if (!is_wp_error($itemResult)) {
               $success[] = $itemResult;
            } else {
@@ -239,9 +273,9 @@
    * @param array $item
    * @return array|WP_Error
    */
   protected function updateOne(array $item):array|WP_Error
   protected function updateOneToService(array $item):array|WP_Error
   {
      return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own updateOne method');
      return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own updateOneToService method');
   }
   /**
@@ -270,6 +304,62 @@
         ]
      ];
   }
   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
   *****************************************************************/
@@ -307,4 +397,48 @@
      }
      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);
   }
}