status_code = $status_code; } public function getStatusCode(): ?int { return $this->status_code; } public function isNotFound(): bool { return $this->status_code === 404; } public function isClientError(): bool { return $this->status_code !== null && $this->status_code >= 400 && $this->status_code < 500; } public function isUnrecoverable(): bool { if ($this->isClientError()) { return true; } $unrecoverable_patterns = [ 'ValueSerp API error', 'Cannot generate content', ]; foreach ($unrecoverable_patterns as $pattern) { if (stripos($this->getMessage(), $pattern) !== false) { return true; } } return false; } } class ContaiPostGenerationOrchestrator { private const META_FEATURED_IMAGE_SOURCE = '_contai_featured_image_source'; private ContaiContentGeneratorService $content_generator; private ContaiCategoryService $category_service; private ContaiWordPressPostCreator $post_creator; private ContaiContentImageProcessor $image_processor; private ContaiImageUploader $image_uploader; private ContaiPostMetadataBuilder $metadata_builder; public function __construct( ContaiContentGeneratorService $content_generator, ContaiCategoryService $category_service, ContaiWordPressPostCreator $post_creator, ContaiContentImageProcessor $image_processor, ContaiImageUploader $image_uploader, ContaiPostMetadataBuilder $metadata_builder ) { $this->content_generator = $content_generator; $this->category_service = $category_service; $this->post_creator = $post_creator; $this->image_processor = $image_processor; $this->image_uploader = $image_uploader; $this->metadata_builder = $metadata_builder; } public static function create(): self { $image_uploader = new ContaiImageUploader(); return new self( ContaiContentGeneratorService::create(), new ContaiCategoryService(), new ContaiWordPressPostCreator(), new ContaiContentImageProcessor($image_uploader), $image_uploader, new ContaiPostMetadataBuilder() ); } public function generate(ContaiKeyword $keyword, array $params): ContaiPostGenerationResult { $content_result = $this->generateContent($keyword, $params); if (!$content_result->isSuccess()) { throw new ContaiContentGenerationException( // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message is not rendered to browser; caught and logged internally. $content_result->getErrorMessage() ?? 'Failed to generate content', // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Status code is an integer, not rendered to browser. $content_result->getStatusCode() ); } return $this->buildPost($keyword, $params, $content_result); } public function createPostFromApiResult(ContaiKeyword $keyword, array $params, array $api_result): ContaiPostGenerationResult { $seo_metadata = $api_result['seo_metadata'] ?? []; $content_result = ContaiContentGenerationResult::success( $api_result['title'] ?? '', $api_result['content'] ?? '', $api_result['images'] ?? [], $api_result['category'] ?? null, $api_result['url'] ?? null, $seo_metadata['metatitle'] ?? null, $seo_metadata['meta_description'] ?? null, $seo_metadata['post_date'] ?? null ); if (empty($content_result->getTitle()) || empty($content_result->getContent())) { throw new ContaiContentGenerationException('Remote job returned empty title or content'); } return $this->buildPost($keyword, $params, $content_result); } private function buildPost(ContaiKeyword $keyword, array $params, ContaiContentGenerationResult $content_result): ContaiPostGenerationResult { $alt_text = $keyword->getTitle() ?: $keyword->getKeyword(); $processed_content = $this->image_processor->process( $content_result->getContent(), $content_result->getImages(), $alt_text ); $title = $content_result->getTitle() ?: $keyword->getKeyword(); $post_id = $this->post_creator->create( $title, $processed_content, $content_result->getSlug(), $content_result->getPostDate(), $content_result->getMetatitle(), $content_result->getMetaDescription() ); $category_id = $this->assignCategoryIfExists($post_id, $content_result->getCategory()); $this->saveMetadata($post_id, $keyword, $params); $this->setFeaturedImageIfExists($post_id, $content_result->getImages(), $alt_text); return new ContaiPostGenerationResult( $post_id, $this->post_creator->getPermalink($post_id), $category_id ); } private function generateContent(ContaiKeyword $keyword, array $params): ContaiContentGenerationResult { $category_names = $this->category_service->getAllCategoryNames(); return $this->content_generator->generateContent( $keyword->getTitle(), $params['lang'] ?? 'en', $params['country'] ?? 'us', $params['image_provider'] ?? 'pexels', $category_names ); } private function assignCategoryIfExists(int $post_id, ?string $category_name): ?int { if (empty($category_name)) { return null; } $category_id = $this->category_service->findOrCreateCategoryId($category_name); if ($category_id !== null) { $this->post_creator->assignCategory($post_id, $category_id); } return $category_id; } private function saveMetadata(int $post_id, ContaiKeyword $keyword, array $params): void { $metadata = $this->metadata_builder->buildFromKeyword($keyword, $params); $this->post_creator->saveMetadata($post_id, $metadata); } private function setFeaturedImageIfExists(int $post_id, array $images, string $alt_text = ''): void { if (empty($images)) { return; } $candidate_urls = array_filter(array_column($images, 'url')); $used_urls = $this->getUsedFeaturedImageUrls($candidate_urls); $selected_url = $this->selectUniqueImageUrl($images, $used_urls); if ($selected_url === null) { return; } // Claim the URL before uploading to prevent concurrent jobs from selecting the same image. update_post_meta($post_id, self::META_FEATURED_IMAGE_SOURCE, $selected_url); $attachment_id = $this->image_uploader->uploadFromUrl($selected_url, $alt_text); if ($attachment_id !== null) { $this->post_creator->setFeaturedImage($post_id, $attachment_id); } else { delete_post_meta($post_id, self::META_FEATURED_IMAGE_SOURCE); } } private function selectUniqueImageUrl(array $images, array $used_urls): ?string { foreach ($images as $image) { $url = $image['url'] ?? null; if (!empty($url) && !in_array($url, $used_urls, true)) { return $url; } } contai_log('All candidate featured images already used on this site, skipping featured image to avoid duplication'); error_log('[ContAI] All candidate featured images already used — post created without featured image'); return null; } private function getUsedFeaturedImageUrls(array $candidate_urls): array { if (empty($candidate_urls)) { return []; } global $wpdb; $placeholders = implode(', ', array_fill(0, count($candidate_urls), '%s')); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- One-time dedup lookup scoped to candidate URLs only. // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $placeholders is generated from array_fill, not user input. $results = $wpdb->get_col( $wpdb->prepare( "SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value IN ($placeholders)", array_merge([self::META_FEATURED_IMAGE_SOURCE], array_values($candidate_urls)) ) ); return is_array($results) ? $results : []; } } class ContaiPostGenerationResult { private int $post_id; private string $post_url; private ?int $category_id; public function __construct(int $post_id, string $post_url, ?int $category_id) { $this->post_id = $post_id; $this->post_url = $post_url; $this->category_id = $category_id; } public function getPostId(): int { return $this->post_id; } public function getPostUrl(): string { return $this->post_url; } public function getCategoryId(): ?int { return $this->category_id; } }