<?php
|
namespace JVBase\managers\queue\executors;
|
|
use JVBase\managers\queue\{Executor, Operation, Progress, Result, Storage};
|
use JVBase\meta\MetaManager;
|
use JVBase\utility\Features;
|
use Exception;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
/**
|
* Executor for content-related queue operations.
|
* Handles: content_update, batch_creation
|
*/
|
final class ContentExecutor implements Executor
|
{
|
private const HANDLED_TYPES = [
|
'content_update',
|
// 'batch_creation',
|
];
|
|
private int $userId;
|
private string $postType;
|
private array $fields = [];
|
|
public function execute(Operation $operation, Progress $progress): Result
|
{
|
$this->userId = $operation->userId;
|
|
if (!in_array($operation->type, self::HANDLED_TYPES)) {
|
throw new Exception("ContentExecutor cannot handle type: {$operation->type}");
|
}
|
|
try {
|
$data = $operation->requestData;
|
|
return match($operation->type) {
|
'content_update' => $this->processContentUpdate($operation, $data, $progress),
|
default => throw new Exception("Unknown type: {$operation->type}")
|
};
|
|
} catch (Exception $e) {
|
JVB()->error()->log(
|
'[ContentExecutor]:execute',
|
$e->getMessage(),
|
[
|
'operation_id' => $operation->id,
|
'operation_type' => $operation->type,
|
'user_id' => $operation->userId,
|
]
|
);
|
|
return new Result(
|
outcome: 'failed',
|
result: ['error' => $e->getMessage()]
|
);
|
}
|
}
|
|
// ─────────────────────────────────────────────────────────────
|
// Content Update
|
// ─────────────────────────────────────────────────────────────
|
|
private function processContentUpdate(Operation $operation, array $data, Progress $progress): Result
|
{
|
$posts = $data['posts'] ?? [];
|
|
if (empty($posts)) {
|
return new Result(
|
outcome: 'failed',
|
result: ['message' => 'No posts to update']
|
);
|
}
|
|
$results = [];
|
$errors = [];
|
$timelineParents = [];
|
$timelineStatus = [];
|
$timelineSharedFields = [];
|
|
foreach ($posts as $id => $postData) {
|
try {
|
$content = $postData['content'] ?? '';
|
|
// New post creation
|
if (str_starts_with((string)$id, 'new')) {
|
$newId = wp_insert_post([
|
'post_author' => $this->userId,
|
'post_type' => jvbCheckBase($content),
|
'post_title' => $postData['post_title'] ?? '',
|
'post_status' => $postData['status'] ?? 'draft',
|
]);
|
|
if (!$newId || is_wp_error($newId)) {
|
$progress->failItem($id, 'Could not create post');
|
continue;
|
}
|
|
$this->savePostFields($newId, $postData);
|
$results[$id] = [
|
'success' => true,
|
'new_id' => $newId,
|
'processed_fields' => array_keys($postData)
|
];
|
$progress->advance();
|
continue;
|
}
|
|
// Existing post update
|
if (!$this->verifyOwnership((int)$id)) {
|
$progress->failItem($id, 'No permission to modify this post');
|
$errors[$id] = 'No permission';
|
continue;
|
}
|
|
$this->savePostFields((int)$id, $postData);
|
|
|
if (Features::forContent($content)->has('is_timeline')) {
|
$post = get_post((int)$id);
|
$parentId = $post->post_parent > 0 ? $post->post_parent : $post->ID;
|
$sharedFields = array_keys(array_filter(JVB_CONTENT[$content]['fields'], function ($field) {
|
return !array_key_exists('for_all', $field) || !$field['for_all'];
|
}));
|
|
if (array_key_exists('post_date', $postData) && !in_array($parentId, $timelineParents)) {
|
$timelineParents[] = $parentId;
|
}
|
if ($parentId === $id) {
|
if (array_key_exists('post_status', $postData) && !array_key_exists($parentId, $timelineStatus)) {
|
$timelineStatus[$parentId] = $postData['post_status'];
|
}
|
|
if (count(array_intersect($sharedFields, array_keys($postData))) > 0) {
|
if (!array_key_exists($parentId, $timelineSharedFields)) {
|
$timelineSharedFields[$parentId] = [];
|
}
|
$temp = array_intersect($sharedFields, array_keys($postData));
|
$timelineSharedFields[$parentId] = array_unique(array_merge($timelineSharedFields[$parentId], $temp));
|
}
|
}
|
}
|
$results[$id] = [
|
'success' => true,
|
'processed_fields' => array_keys($postData)
|
];
|
$progress->advance();
|
|
} catch (Exception $e) {
|
$progress->failItem($id, $e->getMessage());
|
$errors[$id] = $e->getMessage();
|
$results[$id] = [
|
'success' => false,
|
'error' => $e->getMessage()
|
];
|
}
|
}
|
|
try {
|
if (!empty($timelineSharedFields)) {
|
$this->checkSharedFields($timelineSharedFields);
|
}
|
if (!empty($timelineStatus)) {
|
$this->handleTimelineStatusChange($timelineStatus);
|
}
|
if (!empty($timelineParents)) {
|
$this->maybeReorderTimelines($timelineParents);
|
}
|
} catch (Exception $e) {
|
$errors[] = $e->getMessage();
|
}
|
|
|
// Send notification
|
// if (jvbSiteHasNotifications()) {
|
// JVB()->notification()->addNotification(
|
// $this->userId,
|
// 'content_update_complete',
|
// null,
|
// 'Content updates completed!'
|
// );
|
// }
|
|
$outcome = 'success';
|
if (!empty($errors)) {
|
$outcome = count($errors) === count($posts) ? 'failed' : 'partial';
|
}
|
|
|
|
|
return new Result(
|
outcome: $outcome,
|
result: [
|
'posts' => $results,
|
'errors' => $errors,
|
'updated_count' => count(array_filter($results, fn($r) => $r['success'] ?? false)),
|
'failed_count' => count($errors)
|
]
|
);
|
}
|
|
private function savePostFields(int $postId, array $postData): bool
|
{
|
$content = $postData['content'] ?? '';
|
$fields = jvbGetFields($content);
|
|
$allowedFields = array_filter($postData, function ($key) use ($fields) {
|
return array_key_exists($key, $fields);
|
}, ARRAY_FILTER_USE_KEY);
|
|
if (empty($allowedFields)) {
|
return true;
|
}
|
|
$meta = new MetaManager($postId, 'post');
|
return $meta->setAll($allowedFields);
|
}
|
|
// ─────────────────────────────────────────────────────────────
|
// Helpers
|
// ─────────────────────────────────────────────────────────────
|
|
private function verifyOwnership(int $postId): bool
|
{
|
$post = get_post($postId);
|
return $post && (int)$post->post_author === $this->userId;
|
}
|
|
/*************************************************************
|
* TIMELINE HELPERS
|
*************************************************************/
|
protected function maybeReorderTimelines(array $parentIDs):void {
|
foreach ($parentIDs as $parentId) {
|
try {
|
$this->maybeReorderTimeline((int)$parentId);
|
} catch (Exception $e) {
|
error_log("Timeline reorder failed for parent {$parentId}: " . $e->getMessage());
|
}
|
}
|
}
|
protected function maybeReorderTimeline(int $parentID):void
|
{
|
// clean_post_cache($parentID);
|
$parent = get_post($parentID);
|
if (!$parent) {
|
return;
|
}
|
|
$children = get_children([
|
'post_parent' => $parentID,
|
'posts_per_page' => -1,
|
'post_status' => ['publish', 'draft'],
|
]);
|
|
|
if (count($children) === 0) {
|
return;
|
}
|
|
$allPosts = array_merge([$parent], $children);
|
|
// Sort by post_date
|
usort($allPosts, function($a, $b) {
|
return strtotime($a->post_date) <=> strtotime($b->post_date);
|
});
|
|
|
// Check if order changed
|
$needsReorder = false;
|
foreach ($allPosts as $index => $post) {
|
if ($index === 0 && $post->ID !== $parent->ID) {
|
$needsReorder = true;
|
break;
|
}
|
if ($index > 0 && (int)$post->menu_order !== $index) {
|
$needsReorder = true;
|
break;
|
}
|
}
|
|
if (!$needsReorder) {
|
// Just recalculate timelines without reordering
|
$this->recalculateTimelines($allPosts);
|
return;
|
}
|
|
// Handle parent swap if needed
|
$newParent = $allPosts[0];
|
if ($newParent->ID !== $parent->ID) {
|
$this->swapTimelineParent($parent, $newParent, $allPosts);
|
} else {
|
// Just update menu orders and timelines
|
foreach ($allPosts as $index => $post) {
|
if ($index === 0) continue; // Skip parent
|
|
$success = jvb_update_post([
|
'ID' => $post->ID,
|
'post_parent' => $newParent->ID,
|
'menu_order' => $index
|
]);
|
}
|
|
$this->recalculateTimelines($allPosts);
|
}
|
}
|
|
private function recalculateTimelines(array $posts): void
|
{
|
$previousPost = null;
|
$latestTimestamp = 0;
|
|
|
$lastKey = array_key_last($posts);
|
foreach ($posts as $index => $post) {
|
$meta = new MetaManager($post->ID, 'post');
|
if ($index === 0) {
|
$meta->updateValue('timeline', '', false);
|
$previousPost = $post;
|
continue; // Parent has no timeline
|
}
|
|
// Calculate timeline from previous post
|
if ($previousPost) {
|
$timeline = $this->calculateTimeline($previousPost, $post);
|
if ($timeline) {
|
$termId = $this->getOrCreateTerm($timeline, 'timeline');
|
if ($termId) {
|
$success = $meta->updateValue('timeline', $termId, false);
|
}
|
}
|
}
|
|
if ($lastKey === $index) {
|
$latestTimestamp = strtotime($post->post_date);
|
}
|
|
$previousPost = $post;
|
}
|
|
// Update parent's latest_date
|
if ($latestTimestamp > 0) {
|
$success = update_post_meta($posts[0]->ID, BASE . 'latest_date', $latestTimestamp);
|
}
|
}
|
|
private function calculateTimeline(\WP_POST $previous, \WP_POST $current): ?string
|
{
|
$previousDate = strtotime($previous->post_date);
|
$currentDate = strtotime($current->post_date);
|
|
if (!$previousDate || !$currentDate || $currentDate <= $previousDate) {
|
return null;
|
}
|
|
$daysDiff = floor(($currentDate - $previousDate) / (60*60*24));
|
$weeks = floor($daysDiff / 7);
|
if ($weeks === 0) {
|
return 'Less than 1 Week';
|
}
|
if ($weeks < 16) {
|
return $weeks === 1 ? '1 Week' : $weeks . ' Weeks';
|
}
|
|
$previousDateTime = new \DateTime($previous->post_date);
|
$currentDateTime = new \DateTime($current->post_date);
|
|
$interval = $previousDateTime->diff($currentDateTime);
|
$months = ($interval->y * 12) + $interval->m;
|
|
if ($months === 0) {
|
return $weeks . ' Weeks';
|
}
|
|
return ($months === 1) ? '1 Month' : $months . ' Months';
|
}
|
|
private function swapTimelineParent(\WP_Post $oldParent, \WP_Post $newParent, array $allPosts): void
|
{
|
// Swap titles and content
|
$originalTitle = $oldParent->post_title;
|
$originalSlug = $oldParent->post_name;
|
$originalContent = $oldParent->post_content;
|
|
$updateParent = jvb_update_post([
|
'ID' => $oldParent->ID,
|
'post_title' => 'Treatment',
|
'post_name' => sanitize_title('Treatment ' . $newParent->ID),
|
'post_content' => '',
|
]);
|
|
$updateNewParent = jvb_update_post([
|
'ID' => $newParent->ID,
|
'post_title' => $originalTitle,
|
'post_name' => $originalSlug,
|
'post_content' => $originalContent,
|
'post_parent' => 0,
|
'menu_order' => 0
|
]);
|
|
// Clear timeline taxonomy from new parent
|
wp_set_object_terms($newParent->ID, [], BASE . 'timeline', false);
|
|
// Update all other posts to new parent
|
foreach ($allPosts as $index => $post) {
|
if ($index === 0) continue; // Skip new parent
|
|
$title = $post->post_title;
|
if (str_starts_with($title, 'Treatment #')) {
|
$title = 'Treatment #' . $index;
|
}
|
|
$childUpdate = jvb_update_post([
|
'ID' => $post->ID,
|
'post_title' => $title,
|
'post_parent' => $newParent->ID,
|
'menu_order' => $index,
|
]);
|
}
|
|
// Recalculate timelines for all posts
|
$this->recalculateTimelines($allPosts);
|
}
|
|
private function getOrCreateTerm(string $termName, string $taxonomy): ?int
|
{
|
$taxonomy = jvbCheckBase($taxonomy);
|
$term = get_term_by('name', $termName, $taxonomy);
|
|
if (!$term) {
|
$result = wp_insert_term($termName, $taxonomy);
|
if (is_wp_error($result)) {
|
return null;
|
}
|
return $result['term_id'];
|
}
|
|
return $term->term_id;
|
}
|
|
protected function checkSharedFields(array $fields): void
|
{
|
foreach ($fields as $parentID => $shared) {
|
$meta = new MetaManager($parentID, 'post');
|
$values = $meta->getAll($shared);
|
|
$children = get_children([
|
'post_parent' => $parentID,
|
'posts_per_page' => -1,
|
'fields' => 'ids',
|
]);
|
|
if (empty($children)) {
|
continue;
|
}
|
|
foreach ($children as $child) {
|
$childMeta = new MetaManager($child, 'post');
|
$result = $childMeta->setAll($values, false);
|
}
|
}
|
}
|
|
protected function handleTimelineStatusChange(array $updates):void
|
{
|
$updates = array_filter($updates, function ($status) {
|
return in_array($status, ['trash', 'delete', 'publish', 'draft']);
|
});
|
|
foreach ($updates as $parentID => $status) {
|
$children = get_children([
|
'post_parent' => $parentID,
|
'posts_per_page' => -1,
|
'fields' => 'ids'
|
]);
|
if (!empty($children)) {
|
foreach($children as $child) {
|
if ($status === 'trash') {
|
wp_trash_post($child);
|
} elseif ($status === 'delete') {
|
wp_delete_post($child, true);
|
}else {
|
jvb_update_post([
|
'ID' => $child,
|
'post_status' => $status
|
]);
|
}
|
|
}
|
}
|
}
|
}
|
}
|