PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 27.8
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v27.8
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.8, at src/ai/content-planner/application/content-suggestion-command-handler.php

174 lines 5.7 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\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 ) {
73 $this->recent_content_collector = $recent_content_collector;
74 $this->token_manager = $token_manager;
75 $this->request_handler = $request_handler;
76 $this->consent_handler = $consent_handler;
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() );
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 ];
103 if ( $about_page ) {
104 $content['about_page'] = $about_page;
105 }
106 $request_body = [
107 'subject' => [
108 'language' => $command->get_language(),
109 'content' => $content,
110 ],
111 ];
112
113 $request_headers = [
114 'Authorization' => "Bearer $token",
115 'X-Yst-Cohort' => $command->get_editor(),
116 ];
117
118 try {
119
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.
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 );
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.
134 $this->consent_handler->revoke_consent( $command->get_user()->ID );
135 throw new Forbidden_Exception( 'CONSENT_REVOKED', $exception->getCode() );
136 // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
137 }
138
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 = $this->category_repository->find_by_name( $suggestion->category->name );
158
159 $content_suggestion_list->add(
160 new Content_Suggestion(
161 $suggestion->title,
162 $suggestion->intent,
163 $suggestion->explanation,
164 $suggestion->keyphrase,
165 $suggestion->meta_description,
166 $category,
167 ),
168 );
169 }
170
171 return $content_suggestion_list;
172 }
173 }
174