Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 47
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
ArticleTagUpdateJob
0.00% covered (danger)
0.00%
0 / 47
0.00% covered (danger)
0.00%
0 / 4
156
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getJobType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
72
 findOrSaveTag
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2declare(strict_types=1);
3
4namespace App\Job;
5
6use App\Service\Api\AiService;
7use Cake\ORM\Entity;
8use Cake\ORM\Table;
9use Cake\Queue\Job\Message;
10use Interop\Queue\Processor;
11
12/**
13 * ArticleTagUpdateJob
14 *
15 * This job is responsible for updating the tags of an article using AI.
16 * It processes messages from the queue to generate and update tags for articles.
17 */
18class ArticleTagUpdateJob extends AbstractJob
19{
20    /**
21     * Instance of the AI service.
22     *
23     * @var \App\Service\Api\AiService
24     */
25    private AiService $aiService;
26
27    /**
28     * Constructor to allow dependency injection for testing
29     *
30     * @param \App\Service\Api\AiService|null $aiService
31     */
32    public function __construct(?AiService $aiService = null)
33    {
34        $this->aiService = $aiService ?? new AiService();
35    }
36
37    /**
38     * Get the human-readable job type name for logging
39     *
40     * @return string The job type description
41     */
42    protected static function getJobType(): string
43    {
44        return 'article tag update';
45    }
46
47    /**
48     * Executes the job to update article tags.
49     *
50     * This method processes the message, retrieves the article, generates new tags using AI,
51     * and updates the article with the new tags.
52     *
53     * @param \Cake\Queue\Job\Message $message The message containing article data.
54     * @return string|null Returns Processor::ACK on success, Processor::REJECT on failure.
55     */
56    public function execute(Message $message): ?string
57    {
58        if (!$this->validateArguments($message, ['id', 'title'])) {
59            return Processor::REJECT;
60        }
61
62        $id = $message->getArgument('id');
63        $title = $message->getArgument('title');
64
65        $articlesTable = $this->getTable('Articles');
66        $tagsTable = $this->getTable('Tags');
67
68        $article = $articlesTable->get(
69            $id,
70            fields: ['id', 'title', 'body'],
71            contain: ['Tags' => ['fields' => ['id']]],
72        );
73
74        $allTags = $tagsTable->getSimpleThreadedArray();
75
76        return $this->executeWithErrorHandling($id, function () use ($article, $tagsTable, $articlesTable, $allTags) {
77            $tagResult = $this->aiService->generateArticleTags(
78                $allTags,
79                (string)$article->title,
80                (string)strip_tags($article->body),
81            );
82
83            if (isset($tagResult['tags']) && is_array($tagResult['tags'])) {
84                $newTags = [];
85                foreach ($tagResult['tags'] as $rootTag) {
86                    $parentTag = $this->findOrSaveTag($tagsTable, $rootTag['tag'], $rootTag['description']);
87                    $newTags[] = $parentTag;
88                    if (isset($rootTag['children']) && is_array($rootTag['children'])) {
89                        foreach ($rootTag['children'] as $childTag) {
90                            $child = $this->findOrSaveTag(
91                                $tagsTable,
92                                $childTag['tag'],
93                                $childTag['description'],
94                                $parentTag->id,
95                            );
96                            $newTags[] = $child;
97                        }
98                    }
99                }
100
101                $article->tags = $newTags;
102
103                return $articlesTable->save($article, ['validate' => false, 'noMessage' => true]);
104            }
105
106            return false;
107        }, $title);
108    }
109
110    /**
111     * Finds an existing tag by title or creates a new one if it does not exist.
112     *
113     * This method searches for a tag in the provided tags table using the specified title.
114     * If a tag with the given title is not found, it creates a new tag entity with the provided
115     * title, description, and optional parent ID, and saves it to the database.
116     *
117     * @param \Cake\ORM\Table $tagsTable The table instance to search for or save the tag.
118     * @param string $tagTitle The title of the tag to find or create.
119     * @param string $tagDescription The description of the tag to create if it does not exist.
120     * @param int|null $parentId The optional parent ID for the tag, default is null.
121     * @return \Cake\ORM\Entity The found or newly created tag entity.
122     */
123    private function findOrSaveTag(
124        Table $tagsTable,
125        string $tagTitle,
126        string $tagDescription,
127        ?string $parentId = null,
128    ): Entity {
129        $tag = $tagsTable->find()->where(['title' => $tagTitle])->first();
130        if (!$tag) {
131            $tag = $tagsTable->newEmptyEntity();
132            $tag->title = $tagTitle;
133            $tag->description = $tagDescription;
134            $tag->slug = '';
135            $tag->parent_id = $parentId;
136            $tagsTable->save($tag);
137        }
138
139        return $tag;
140    }
141}