PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.4
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.4
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 / bulk-editor / application / updates / bulk-updater.php

bulk-updater.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 28.4, at src/bulk-editor/application/updates/bulk-updater.php

176 lines 5.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
4 namespace Yoast\WP\SEO\Bulk_Editor\Application\Updates;
5
6 use Exception;
7 use Yoast\WP\SEO\Bulk_Editor\Domain\Updates\Post_Update;
8 use Yoast\WP\SEO\Bulk_Editor\Domain\Updates\Post_Update_Collection;
9 use Yoast\WP\SEO\Bulk_Editor\Domain\Updates\Update_Error;
10 use Yoast\WP\SEO\Bulk_Editor\Domain\Updates\Update_Result;
11 use Yoast\WP\SEO\Bulk_Editor\Domain\Updates\Update_Result_Collection;
12 use Yoast\WP\SEO\Bulk_Editor\Domain\Updates\Update_Type;
13 use YoastSEO_Vendor\Psr\Log\LoggerAwareInterface;
14 use YoastSEO_Vendor\Psr\Log\LoggerAwareTrait;
15 use YoastSEO_Vendor\Psr\Log\NullLogger;
16
17 /**
18 * Applies a batch of post updates, independently per post.
19 */
20 class Bulk_Updater implements LoggerAwareInterface {
21
22 use LoggerAwareTrait;
23
24 /**
25 * The post access checker.
26 *
27 * @var Post_Access_Checker_Interface
28 */
29 private $post_access_checker;
30
31 /**
32 * The meta writer.
33 *
34 * @var Meta_Writer_Interface
35 */
36 private $meta_writer;
37
38 /**
39 * The field renderer.
40 *
41 * @var Field_Renderer_Interface
42 */
43 private $field_renderer;
44
45 /**
46 * The constructor.
47 *
48 * @param Post_Access_Checker_Interface $post_access_checker The post access checker.
49 * @param Meta_Writer_Interface $meta_writer The meta writer.
50 * @param Field_Renderer_Interface $field_renderer The field renderer.
51 */
52 public function __construct(
53 Post_Access_Checker_Interface $post_access_checker,
54 Meta_Writer_Interface $meta_writer,
55 Field_Renderer_Interface $field_renderer
56 ) {
57 $this->post_access_checker = $post_access_checker;
58 $this->meta_writer = $meta_writer;
59 $this->field_renderer = $field_renderer;
60 $this->logger = new NullLogger();
61 }
62
63 /**
64 * Applies the given post updates. Updates are applied independently: one failing
65 * update does not block the others.
66 *
67 * @param Update_Type $type The appearance the updates target.
68 * @param Post_Update_Collection $updates The post updates to apply.
69 *
70 * @return Update_Result_Collection The result per post update.
71 */
72 public function update( Update_Type $type, Post_Update_Collection $updates ): Update_Result_Collection {
73 $results = new Update_Result_Collection();
74
75 foreach ( $updates->get() as $update ) {
76 $results->add( $this->apply( $type, $update ) );
77 }
78
79 return $results;
80 }
81
82 /**
83 * Applies a single post update.
84 *
85 * @param Update_Type $type The appearance the update targets.
86 * @param Post_Update $update The post update to apply.
87 *
88 * @return Update_Result The result of the update.
89 */
90 private function apply( Update_Type $type, Post_Update $update ): Update_Result {
91 $post_id = $update->get_post_id();
92
93 if ( ! $this->post_access_checker->exists( $post_id ) ) {
94 return Update_Result::for_failure( $post_id, Update_Error::NOT_FOUND );
95 }
96
97 if ( ! $this->post_access_checker->is_supported_type( $post_id ) ) {
98 return Update_Result::for_failure( $post_id, Update_Error::INVALID_POST_TYPE );
99 }
100
101 if ( ! $this->post_access_checker->can_edit( $post_id ) ) {
102 return Update_Result::for_failure( $post_id, Update_Error::FORBIDDEN );
103 }
104
105 $saved_focus_keyphrase = null;
106 try {
107 if ( $update->has_title() ) {
108 $this->meta_writer->write_title( $type, $post_id, $update->get_title() );
109 }
110
111 if ( $update->has_description() ) {
112 $this->meta_writer->write_description( $type, $post_id, $update->get_description() );
113 }
114
115 if ( $update->has_focus_keyphrase() ) {
116 $saved_focus_keyphrase = $this->meta_writer->write_focus_keyphrase( $post_id, $update->get_focus_keyphrase() );
117 }
118 } catch ( Exception $exception ) {
119 $this->logger->warning(
120 'Bulk update failed to save post {post_id}: {error}',
121 [
122 'post_id' => $post_id,
123 'error' => $exception->getMessage(),
124 ],
125 );
126
127 return Update_Result::for_failure( $post_id, Update_Error::SAVE_FAILED );
128 }
129
130 return Update_Result::for_success(
131 $post_id,
132 $this->render_fields( $type, $post_id ),
133 $this->sanitized_fields( $saved_focus_keyphrase ),
134 );
135 }
136
137 /**
138 * Renders the search fields with replacement variables resolved, for re-scoring after a save.
139 *
140 * Only called for search updates; social updates carry no scored fields.
141 *
142 * @param Update_Type $type The appearance the update targets.
143 * @param int $post_id The ID of the post.
144 *
145 * @return array<string, string> The rendered fields, keyed by field.
146 */
147 private function render_fields( Update_Type $type, int $post_id ): array {
148 if ( ! $type->is_search() ) {
149 return [];
150 }
151
152 return [
153 'seo_title' => $this->field_renderer->render( $post_id, 'title' ),
154 'meta_description' => $this->field_renderer->render( $post_id, 'metadesc' ),
155 ];
156 }
157
158 /**
159 * Builds the sanitized-literals payload for the response.
160 *
161 * The focus keyphrase is echoed back when it was part of the update, so the frontend can detect
162 * when sanitization silently altered the submitted value (e.g. HTML stripped).
163 *
164 * @param string|null $saved_focus_keyphrase The sanitized focus keyphrase that was stored, or null if not updated.
165 *
166 * @return array<string, string> The sanitized literals, keyed by field.
167 */
168 private function sanitized_fields( ?string $saved_focus_keyphrase ): array {
169 if ( $saved_focus_keyphrase === null ) {
170 return [];
171 }
172
173 return [ 'focus_keyphrase' => $saved_focus_keyphrase ];
174 }
175 }
176