PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 27.9
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v27.9
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / src / ai / content-planner / application / content-suggestion-command-handler.php

content-suggestion-command-handler.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 27.9, at src/ai/content-planner/application/content-suggestion-command-handler.php

199 lines 6.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 {
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 try {
100 $token = $this->token_manager->get_or_request_access_token( $command->get_user() );
101 } catch ( Forbidden_Exception $exception ) {
102 // Follow the API in the consent being revoked (Use case: user sent an e-mail to revoke?).
103 // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive.
104 $this->consent_handler->revoke_consent( $command->get_user()->ID );
105 throw new Forbidden_Exception( 'CONSENT_REVOKED', $exception->getCode() );
106 // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
107 }
108 $recent_content = $post_list->to_array();
109
110 $content = [
111 'posts' => $recent_content,
112 ];
113 if ( $about_page ) {
114 $content['about_page'] = $about_page;
115 }
116 $request_body = [
117 'subject' => [
118 'language' => $command->get_language(),
119 'content' => $content,
120 ],
121 ];
122
123 $request_headers = [
124 'Authorization' => "Bearer $token",
125 'X-Yst-Cohort' => $command->get_editor(),
126 ];
127
128 try {
129
130 $response = $this->request_handler->handle( new Request( '/content-planner/next-post-suggestions', $request_body, $request_headers ) );
131 } catch ( Unauthorized_Exception $exception ) {
132 // Delete the stored JWT tokens, as they appear to be no longer valid.
133 $this->token_manager->clear_tokens( (string) $command->get_user()->ID );
134
135 if ( ! $retry_on_unauthorized ) {
136 throw $exception;
137 }
138
139 // Try again once more by fetching a new set of tokens and trying the suggestions endpoint again.
140 return $this->handle( $command, false );
141 } catch ( Forbidden_Exception $exception ) {
142 // Follow the API in the consent being revoked (Use case: user sent an e-mail to revoke?).
143 // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive.
144 $this->consent_handler->revoke_consent( $command->get_user()->ID );
145 throw new Forbidden_Exception( 'CONSENT_REVOKED', $exception->getCode() );
146 // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
147 }
148
149 return $this->build_response( $response, $post_list );
150 }
151
152 /**
153 * Builds a response object bundling the content suggestions with the recent content used to generate them.
154 *
155 * @param Response $response The API response.
156 * @param Post_List $recent_content The recent content passed to the AI API.
157 *
158 * @return Content_Suggestion_Response The response containing suggestions and recent content.
159 */
160 public function build_response( Response $response, Post_List $recent_content ): Content_Suggestion_Response {
161 return new Content_Suggestion_Response(
162 $this->build_suggestions_array( $response ),
163 $recent_content,
164 );
165 }
166
167 /**
168 * Builds a list of content suggestions from the API response.
169 *
170 * @param Response $response The API response.
171 *
172 * @return Content_Suggestion_List The list of content suggestions.
173 */
174 public function build_suggestions_array( Response $response ): Content_Suggestion_List {
175 $content_suggestion_list = new Content_Suggestion_List();
176 $json = \json_decode( $response->get_body() );
177
178 if ( $json === null || ! isset( $json->choices ) ) {
179 return $content_suggestion_list;
180 }
181 foreach ( $json->choices as $suggestion ) {
182 $category = $this->category_repository->find_by_name( $suggestion->category->name );
183
184 $content_suggestion_list->add(
185 new Content_Suggestion(
186 $suggestion->title,
187 $suggestion->intent,
188 $suggestion->explanation,
189 $suggestion->keyphrase,
190 $suggestion->meta_description,
191 $category,
192 ),
193 );
194 }
195
196 return $content_suggestion_list;
197 }
198 }
199