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

Yoast / wordpress-seo / 94231500761e6534313113fbdb57c9a30a831068

17 Apr 2026 09:50AM UTC coverage: 53.554% (+0.6%) from 52.98%
94231500761e6534313113fbdb57c9a30a831068

push

github

web-flow
Merge pull request #23168 from Yoast/1142-connect-content-planner-frontend-with-backend-endpoints-

1142 connect content planner frontend with backend endpoints

9171 of 16830 branches covered (54.49%)

Branch coverage included in aggregate %.

128 of 199 new or added lines in 22 files covered. (64.32%)

33 existing lines in 5 files now uncovered.

35135 of 65902 relevant lines covered (53.31%)

45645.4 hits per line

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

0.0
/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\Infrastructure\Recent_Content\Recent_Content_Collector;
11
use Yoast\WP\SEO\AI\HTTP_Request\Application\Request_Handler;
12
use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Forbidden_Exception;
13
use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Unauthorized_Exception;
14
use Yoast\WP\SEO\AI\HTTP_Request\Domain\Request;
15
use Yoast\WP\SEO\AI\HTTP_Request\Domain\Response;
16

17
/**
18
 * Handles the content suggestion command.
19
 */
20
class Content_Suggestion_Command_Handler {
21

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

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

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

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

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

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

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

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

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

118
                try {
119

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

125
                        if ( ! $retry_on_unauthorized ) {
×
126
                                throw $exception;
×
127
                        }
128

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

UNCOV
139
                return $this->build_suggestions_array( $response );
×
140
        }
141

142
        /**
143
         * Builds a list of content suggestions from the API response.
144
         *
145
         * @param Response $response The API response.
146
         *
147
         * @return Content_Suggestion_List The list of content suggestions.
148
         */
149
        public function build_suggestions_array( Response $response ): Content_Suggestion_List {
×
150
                $content_suggestion_list = new Content_Suggestion_List();
×
151
                $json                    = \json_decode( $response->get_body() );
×
152

153
                if ( $json === null || ! isset( $json->choices ) ) {
×
154
                        return $content_suggestion_list;
×
155
                }
156
                foreach ( $json->choices as $suggestion ) {
×
157
                        $category = isset( $suggestion->category->title ) ? $this->category_repository->find_by_name( $suggestion->category->title ) : null;
×
158

UNCOV
159
                        $content_suggestion_list->add(
×
UNCOV
160
                                new Content_Suggestion(
×
161
                                        $suggestion->title,
×
UNCOV
162
                                        $suggestion->intent,
×
UNCOV
163
                                        $suggestion->explanation,
×
UNCOV
164
                                        $suggestion->keyphrase,
×
UNCOV
165
                                        $suggestion->meta_description,
×
UNCOV
166
                                        $category,
×
UNCOV
167
                                ),
×
UNCOV
168
                        );
×
169
                }
170

UNCOV
171
                return $content_suggestion_list;
×
172
        }
173
}
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