<?php
|
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, 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);
|
}
|
}
|