• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Yoast / wordpress-seo / b5534c8d1ae27fed913e9e9af3d6b5947f49053d

19 May 2026 01:45PM UTC coverage: 43.931%. First build
b5534c8d1ae27fed913e9e9af3d6b5947f49053d

Pull #23269

github

web-flow
Merge c0dbf7c9e into 0fdb0ef2f
Pull Request #23269: 1155 cache get recently modified posts results

3225 of 10518 branches covered (30.66%)

Branch coverage included in aggregate %.

50 of 53 new or added lines in 7 files covered. (94.34%)

26890 of 58033 relevant lines covered (46.34%)

5.47 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

37.29
/src/ai/content-planner/application/content-suggestion-command-handler.php
1
<?php
2
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
3

4
namespace Yoast\WP\SEO\AI\Content_Planner\Application;
5

6
use Yoast\WP\SEO\AI\Authorization\Application\Token_Manager;
7
use Yoast\WP\SEO\AI\Consent\Application\Consent_Handler;
8
use Yoast\WP\SEO\AI\Content_Planner\Domain\Content_Suggestion;
9
use Yoast\WP\SEO\AI\Content_Planner\Domain\Content_Suggestion_List;
10
use Yoast\WP\SEO\AI\Content_Planner\Domain\Content_Suggestion_Response;
11
use Yoast\WP\SEO\AI\Content_Planner\Domain\Post_List;
12
use Yoast\WP\SEO\AI\Content_Planner\Infrastructure\Recent_Content\Recent_Content_Collector;
13
use Yoast\WP\SEO\AI\HTTP_Request\Application\Request_Handler;
14
use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Forbidden_Exception;
15
use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Unauthorized_Exception;
16
use Yoast\WP\SEO\AI\HTTP_Request\Domain\Request;
17
use Yoast\WP\SEO\AI\HTTP_Request\Domain\Response;
18

19
/**
20
 * Handles the content suggestion command.
21
 */
22
class Content_Suggestion_Command_Handler {
23

24
        /**
25
         * The recent content collector.
26
         *
27
         * @var Recent_Content_Collector
28
         */
29
        private $recent_content_collector;
30

31
        /**
32
         * The token manager.
33
         *
34
         * @var Token_Manager
35
         */
36
        private $token_manager;
37

38
        /**
39
         * The request handler.
40
         *
41
         * @var Request_Handler
42
         */
43
        private $request_handler;
44

45
        /**
46
         * The consent handler.
47
         *
48
         * @var Consent_Handler
49
         */
50
        private $consent_handler;
51

52
        /**
53
         * The category repository.
54
         *
55
         * @var Category_Repository_Interface
56
         */
57
        private $category_repository;
58

59
        /**
60
         * The constructor.
61
         *
62
         * @param Recent_Content_Collector      $recent_content_collector The recent content collector.
63
         * @param Token_Manager                 $token_manager            The token manager.
64
         * @param Request_Handler               $request_handler          The request handler.
65
         * @param Consent_Handler               $consent_handler          The consent handler.
66
         * @param Category_Repository_Interface $category_repository      The category repository.
67
         */
68
        public function __construct(
×
69
                Recent_Content_Collector $recent_content_collector,
70
                Token_Manager $token_manager,
71
                Request_Handler $request_handler,
72
                Consent_Handler $consent_handler,
73
                Category_Repository_Interface $category_repository
74
        ) {
75
                $this->recent_content_collector = $recent_content_collector;
×
76
                $this->token_manager            = $token_manager;
×
77
                $this->request_handler          = $request_handler;
×
78
                $this->consent_handler          = $consent_handler;
×
79
                $this->category_repository      = $category_repository;
×
80
        }
81

82
        /**
83
         * Handles the content suggestion command by collecting recent content and requesting suggestions from the AI API.
84
         *
85
         * @param Content_Suggestion_Command $command               The content suggestion command.
86
         * @param bool                       $retry_on_unauthorized Whether to retry on unauthorized response.
87
         *
88
         * @throws Unauthorized_Exception When the API returns an unauthorized response and retry is exhausted.
89
         * @throws Forbidden_Exception    When consent has been revoked.
90
         *
91
         * @return Content_Suggestion_Response The response containing suggestions and recent content.
92
         */
93
        public function handle(
×
94
                Content_Suggestion_Command $command,
95
                bool $retry_on_unauthorized = true
96
        ): Content_Suggestion_Response {
NEW
97
                $post_list      = $this->recent_content_collector->collect( $command->get_post_type() );
×
98
                $about_page     = $this->recent_content_collector->collect_about_page( $command->get_post_type() );
×
99
                $token          = $this->token_manager->get_or_request_access_token( $command->get_user() );
×
NEW
100
                $recent_content = $post_list->to_array();
×
101

102
                $content = [
×
103
                        'posts' => $recent_content,
×
104
                ];
×
105
                if ( $about_page ) {
×
106
                        $content['about_page'] = $about_page;
×
107
                }
108
                $request_body = [
×
109
                        'subject' => [
×
110
                                'language' => $command->get_language(),
×
111
                                'content'  => $content,
×
112
                        ],
×
113
                ];
×
114

115
                $request_headers = [
×
116
                        'Authorization' => "Bearer $token",
×
117
                        'X-Yst-Cohort'  => $command->get_editor(),
×
118
                ];
×
119

120
                try {
121

122
                        $response = $this->request_handler->handle( new Request( '/content-planner/next-post-suggestions', $request_body, $request_headers ) );
×
123
                } catch ( Unauthorized_Exception $exception ) {
×
124
                        // Delete the stored JWT tokens, as they appear to be no longer valid.
125
                        $this->token_manager->clear_tokens( (string) $command->get_user()->ID );
×
126

127
                        if ( ! $retry_on_unauthorized ) {
×
128
                                throw $exception;
×
129
                        }
130

131
                        // Try again once more by fetching a new set of tokens and trying the suggestions endpoint again.
132
                        return $this->handle( $command, false );
×
133
                } catch ( Forbidden_Exception $exception ) {
×
134
                        // Follow the API in the consent being revoked (Use case: user sent an e-mail to revoke?).
135
                        // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive.
136
                        $this->consent_handler->revoke_consent( $command->get_user()->ID );
×
137
                        throw new Forbidden_Exception( 'CONSENT_REVOKED', $exception->getCode() );
×
138
                        // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
139
                }
140

NEW
141
                return $this->build_response( $response, $post_list );
×
142
        }
143

144
        /**
145
         * Builds a response object bundling the content suggestions with the recent content used to generate them.
146
         *
147
         * @param Response  $response       The API response.
148
         * @param Post_List $recent_content The recent content passed to the AI API.
149
         *
150
         * @return Content_Suggestion_Response The response containing suggestions and recent content.
151
         */
152
        public function build_response( Response $response, Post_List $recent_content ): Content_Suggestion_Response {
4✔
153
                return new Content_Suggestion_Response(
4✔
154
                        $this->build_suggestions_array( $response ),
4✔
155
                        $recent_content,
4✔
156
                );
4✔
157
        }
158

159
        /**
160
         * Builds a list of content suggestions from the API response.
161
         *
162
         * @param Response $response The API response.
163
         *
164
         * @return Content_Suggestion_List The list of content suggestions.
165
         */
166
        public function build_suggestions_array( Response $response ): Content_Suggestion_List {
6✔
167
                $content_suggestion_list = new Content_Suggestion_List();
6✔
168
                $json                    = \json_decode( $response->get_body() );
6✔
169

170
                if ( $json === null || ! isset( $json->choices ) ) {
6✔
171
                        return $content_suggestion_list;
×
172
                }
173
                foreach ( $json->choices as $suggestion ) {
6✔
174
                        $category = $this->category_repository->find_by_name( $suggestion->category->name );
6✔
175

176
                        $content_suggestion_list->add(
6✔
177
                                new Content_Suggestion(
6✔
178
                                        $suggestion->title,
6✔
179
                                        $suggestion->intent,
6✔
180
                                        $suggestion->explanation,
6✔
181
                                        $suggestion->keyphrase,
6✔
182
                                        $suggestion->meta_description,
6✔
183
                                        $category,
6✔
184
                                ),
6✔
185
                        );
6✔
186
                }
187

188
                return $content_suggestion_list;
6✔
189
        }
190
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc