PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / trunk
Yoast SEO – Advanced SEO with real-time guidance and built-in AI vtrunk
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
← All changes | src/ai/content-planner/application/content-suggestion-command-handler.php +53 -63 27.6 → trunk View file →
@@ -2,17 +2,19 @@
2 2 // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
3 3
4 4 namespace Yoast\WP\SEO\AI\Content_Planner\Application;
5 5
6 -use Yoast\WP\SEO\AI\Authorization\Application\Token_Manager;
6 +use Yoast\WP\SEO\AI\Authentication\Application\AI_Request_Sender_Factory;
7 7 use Yoast\WP\SEO\AI\Consent\Application\Consent_Handler;
8 8 use Yoast\WP\SEO\AI\Content_Planner\Domain\Content_Suggestion;
9 9 use Yoast\WP\SEO\AI\Content_Planner\Domain\Content_Suggestion_List;
10 +use Yoast\WP\SEO\AI\Content_Planner\Domain\Content_Suggestion_Parameters;
11 +use Yoast\WP\SEO\AI\Content_Planner\Domain\Content_Suggestion_Response;
12 +use Yoast\WP\SEO\AI\Content_Planner\Domain\Post_List;
10 13 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 14 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Forbidden_Exception;
15 +use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Insufficient_Scope_Exception;
13 16 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Unauthorized_Exception;
14 -use Yoast\WP\SEO\AI\HTTP_Request\Domain\Request;
15 17 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Response;
16 18
17 19 /**
18 20 * Handles the content suggestion command.
@@ -26,22 +28,15 @@
26 28 */
27 29 private $recent_content_collector;
28 30
29 31 /**
30 - * The token manager.
32 + * The auth strategy factory.
31 33 *
32 - * @var Token_Manager
34 + * @var AI_Request_Sender_Factory
33 35 */
34 - private $token_manager;
36 + private $ai_request_sender_factory;
35 37
36 38 /**
37 - * The request handler.
38 - *
39 - * @var Request_Handler
40 - */
41 - private $request_handler;
42 -
43 - /**
44 39 * The consent handler.
45 40 *
46 41 * @var Consent_Handler
47 42 */
@@ -56,88 +51,83 @@
56 51
57 52 /**
58 53 * The constructor.
59 54 *
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.
55 + * @param Recent_Content_Collector $recent_content_collector The recent content collector.
56 + * @param AI_Request_Sender_Factory $ai_request_sender_factory The auth strategy factory.
57 + * @param Consent_Handler $consent_handler The consent handler.
58 + * @param Category_Repository_Interface $category_repository The category repository.
65 59 */
66 60 public function __construct(
67 61 Recent_Content_Collector $recent_content_collector,
68 - Token_Manager $token_manager,
69 - Request_Handler $request_handler,
62 + AI_Request_Sender_Factory $ai_request_sender_factory,
70 63 Consent_Handler $consent_handler,
71 64 Category_Repository_Interface $category_repository
72 65 ) {
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;
66 + $this->recent_content_collector = $recent_content_collector;
67 + $this->ai_request_sender_factory = $ai_request_sender_factory;
68 + $this->consent_handler = $consent_handler;
69 + $this->category_repository = $category_repository;
78 70 }
79 71
72 + // phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- PHPCS doesn't track exceptions thrown by called services.
73 +
80 74 /**
81 75 * Handles the content suggestion command by collecting recent content and requesting suggestions from the AI API.
82 76 *
83 - * @param Content_Suggestion_Command $command The content suggestion command.
84 - * @param bool $retry_on_unauthorized Whether to retry on unauthorized response.
77 + * @param Content_Suggestion_Command $command The content suggestion command.
85 78 *
86 - * @throws Unauthorized_Exception When the API returns an unauthorized response and retry is exhausted.
87 - * @throws Forbidden_Exception When consent has been revoked.
79 + * @throws Unauthorized_Exception When the API returns an unauthorized response and retry is exhausted.
80 + * @throws Forbidden_Exception When consent has been revoked.
81 + * @throws Insufficient_Scope_Exception When the OAuth path's token is missing the required scope.
88 82 *
89 - * @return Content_Suggestion_List A list of content suggestions.
83 + * @return Content_Suggestion_Response The response containing suggestions and recent content.
90 84 */
91 - public function handle(
92 - Content_Suggestion_Command $command,
93 - bool $retry_on_unauthorized = true
94 - ): Content_Suggestion_List {
85 + public function handle( Content_Suggestion_Command $command ): Content_Suggestion_Response {
95 86 $recent_content = $this->recent_content_collector->collect( $command->get_post_type() );
96 87 $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 88
100 89 $content = [
101 - 'posts' => $recent_content,
90 + 'posts' => $recent_content->to_array(),
102 91 ];
103 92 if ( $about_page ) {
104 93 $content['about_page'] = $about_page;
105 94 }
106 - $request_body = [
107 - 'subject' => [
108 - 'language' => $command->get_language(),
109 - 'content' => $content,
110 - ],
111 - ];
95 + $parameters = new Content_Suggestion_Parameters(
96 + $command->get_user(),
97 + $command->get_language(),
98 + $content,
99 + $command->get_editor(),
100 + );
112 101
113 - $request_headers = [
114 - 'Authorization' => "Bearer $token",
115 - 'X-Yst-Cohort' => $command->get_editor(),
116 - ];
117 -
118 102 try {
103 + $sender = $this->ai_request_sender_factory->create( $command->get_user() );
104 + $response = $sender->get_content_suggestions( $parameters );
105 + } catch ( Insufficient_Scope_Exception $exception ) {
119 106
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 );
107 + throw $exception;
131 108 } 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 109 $this->consent_handler->revoke_consent( $command->get_user()->ID );
110 + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive.
135 111 throw new Forbidden_Exception( 'CONSENT_REVOKED', $exception->getCode() );
136 - // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
137 112 }
138 113
139 - return $this->build_suggestions_array( $response );
114 + return $this->build_response( $response, $recent_content );
115 + }
116 +
117 + /**
118 + * Builds a response object bundling the content suggestions with the recent content used to generate them.
119 + *
120 + * @param Response $response The API response.
121 + * @param Post_List $recent_content The recent content passed to the AI API.
122 + *
123 + * @return Content_Suggestion_Response The response containing suggestions and recent content.
124 + */
125 + public function build_response( Response $response, Post_List $recent_content ): Content_Suggestion_Response {
126 + return new Content_Suggestion_Response(
127 + $this->build_suggestions_array( $response ),
128 + $recent_content,
129 + );
140 130 }
141 131
142 132 /**
143 133 * Builds a list of content suggestions from the API response.