PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.2
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.2
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-outline-command-handler.php

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

150 lines 4.8 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\Authentication\Application\AI_Request_Sender_Factory;
7 use Yoast\WP\SEO\AI\Consent\Application\Consent_Handler;
8 use Yoast\WP\SEO\AI\Content_Planner\Domain\Content_Outline_Parameters;
9 use Yoast\WP\SEO\AI\Content_Planner\Domain\Section;
10 use Yoast\WP\SEO\AI\Content_Planner\Domain\Section_List;
11 use Yoast\WP\SEO\AI\Content_Planner\Infrastructure\Recent_Content\Recent_Content_Collector;
12 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Forbidden_Exception;
13 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Insufficient_Scope_Exception;
14 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Unauthorized_Exception;
15 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Response;
16
17 /**
18 * Handles the content outline command.
19 */
20 class Content_Outline_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 auth strategy factory.
31 *
32 * @var AI_Request_Sender_Factory
33 */
34 private $ai_request_sender_factory;
35
36 /**
37 * The consent handler.
38 *
39 * @var Consent_Handler
40 */
41 private $consent_handler;
42
43 /**
44 * The constructor.
45 *
46 * @param Recent_Content_Collector $recent_content_collector The recent content collector.
47 * @param AI_Request_Sender_Factory $ai_request_sender_factory The auth strategy factory.
48 * @param Consent_Handler $consent_handler The consent handler.
49 */
50 public function __construct(
51 Recent_Content_Collector $recent_content_collector,
52 AI_Request_Sender_Factory $ai_request_sender_factory,
53 Consent_Handler $consent_handler
54 ) {
55 $this->recent_content_collector = $recent_content_collector;
56 $this->ai_request_sender_factory = $ai_request_sender_factory;
57 $this->consent_handler = $consent_handler;
58 }
59
60 // phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- PHPCS doesn't track exceptions thrown by called services.
61
62 /**
63 * Handles the content outline command by collecting recent content and requesting an outline from the AI API.
64 *
65 * @param Content_Outline_Command $command The content outline command.
66 *
67 * @throws Unauthorized_Exception When the API returns an unauthorized response and retry is exhausted.
68 * @throws Forbidden_Exception When consent has been revoked.
69 * @throws Insufficient_Scope_Exception When the OAuth path's token is missing the required scope.
70 *
71 * @return Section_List A list of outline sections.
72 */
73 public function handle( Content_Outline_Command $command ): Section_List {
74 $recent_content = $command->get_recent_content();
75 $about_page = $this->recent_content_collector->collect_about_page( $command->get_post_type() );
76
77 $existing_posts = \array_map(
78 static function ( $post ) {
79 return [
80 'title' => $post['title'],
81 'description' => $post['description'],
82 ];
83 },
84 $recent_content,
85 );
86
87 $content = [
88 'new_post_metadata' => [
89 'title' => $command->get_title(),
90 'intent' => $command->get_intent(),
91 'explanation' => $command->get_explanation(),
92 'keyphrase' => $command->get_keyphrase(),
93 'meta_description' => $command->get_meta_description(),
94 'category' => $command->get_category()->to_array(),
95 ],
96 'existing_posts' => $existing_posts,
97 ];
98 if ( $about_page ) {
99 $content['about_page'] = $about_page;
100 }
101
102 $parameters = new Content_Outline_Parameters(
103 $command->get_user(),
104 $command->get_language(),
105 $content,
106 $command->get_editor(),
107 );
108
109 try {
110 $sender = $this->ai_request_sender_factory->create( $command->get_user() );
111 $response = $sender->get_content_outline_suggestions( $parameters );
112 } catch ( Insufficient_Scope_Exception $exception ) {
113 throw $exception;
114 } catch ( Forbidden_Exception $exception ) {
115 $this->consent_handler->revoke_consent( $command->get_user()->ID );
116 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
117 throw new Forbidden_Exception( 'CONSENT_REVOKED', $exception->getCode() );
118
119 }
120
121 return $this->build_outline( $response );
122 }
123
124 /**
125 * Builds a list of outline sections from the API response.
126 *
127 * @param Response $response The API response.
128 *
129 * @return Section_List The list of outline sections.
130 */
131 private function build_outline( Response $response ): Section_List {
132 $section_list = new Section_List();
133 $json = \json_decode( $response->get_body() );
134
135 if ( $json === null || ! isset( $json->choices ) ) {
136 return $section_list;
137 }
138 foreach ( $json->choices as $choice ) {
139 $section_list->add(
140 new Section(
141 ( $choice->content_notes ?? [] ),
142 ( $choice->subheading_text ?? null ),
143 ),
144 );
145 }
146
147 return $section_list;
148 }
149 }
150