PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.3
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.3
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 19.1 All 128 releases
wordpress-seo / src / builders / indexable-builder.php

indexable-builder.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 28.3, at src/builders/indexable-builder.php

481 lines 15.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Yoast\WP\SEO\Builders;
4
5 use Throwable;
6 use Yoast\WP\SEO\Exceptions\Indexable\Indexing_Failed_Exception;
7 use Yoast\WP\SEO\Exceptions\Indexable\Not_Built_Exception;
8 use Yoast\WP\SEO\Exceptions\Indexable\Source_Exception;
9 use Yoast\WP\SEO\Helpers\Indexable_Helper;
10 use Yoast\WP\SEO\Loggers\Logger;
11 use Yoast\WP\SEO\Models\Indexable;
12 use Yoast\WP\SEO\Repositories\Indexable_Repository;
13 use Yoast\WP\SEO\Services\Indexables\Indexable_Version_Manager;
14
15 /**
16 * Builder for the indexables.
17 *
18 * Creates all the indexables.
19 */
20 class Indexable_Builder {
21
22 /**
23 * The author builder.
24 *
25 * @var Indexable_Author_Builder
26 */
27 private $author_builder;
28
29 /**
30 * The post builder.
31 *
32 * @var Indexable_Post_Builder
33 */
34 private $post_builder;
35
36 /**
37 * The term builder.
38 *
39 * @var Indexable_Term_Builder
40 */
41 private $term_builder;
42
43 /**
44 * The home page builder.
45 *
46 * @var Indexable_Home_Page_Builder
47 */
48 private $home_page_builder;
49
50 /**
51 * The post type archive builder.
52 *
53 * @var Indexable_Post_Type_Archive_Builder
54 */
55 private $post_type_archive_builder;
56
57 /**
58 * The data archive builder.
59 *
60 * @var Indexable_Date_Archive_Builder
61 */
62 private $date_archive_builder;
63
64 /**
65 * The system page builder.
66 *
67 * @var Indexable_System_Page_Builder
68 */
69 private $system_page_builder;
70
71 /**
72 * The indexable hierarchy builder.
73 *
74 * @var Indexable_Hierarchy_Builder
75 */
76 private $hierarchy_builder;
77
78 /**
79 * The primary term builder
80 *
81 * @var Primary_Term_Builder
82 */
83 private $primary_term_builder;
84
85 /**
86 * The link builder
87 *
88 * @var Indexable_Link_Builder
89 */
90 private $link_builder;
91
92 /**
93 * The indexable repository.
94 *
95 * @var Indexable_Repository
96 */
97 private $indexable_repository;
98
99 /**
100 * The indexable helper.
101 *
102 * @var Indexable_Helper
103 */
104 protected $indexable_helper;
105
106 /**
107 * The Indexable Version Manager.
108 *
109 * @var Indexable_Version_Manager
110 */
111 protected $version_manager;
112
113 /**
114 * The logger.
115 *
116 * @var Logger
117 */
118 protected $logger;
119
120 /**
121 * Returns the instance of this class constructed through the ORM Wrapper.
122 *
123 * @param Indexable_Author_Builder $author_builder The author builder for creating missing indexables.
124 * @param Indexable_Post_Builder $post_builder The post builder for creating missing indexables.
125 * @param Indexable_Term_Builder $term_builder The term builder for creating missing indexables.
126 * @param Indexable_Home_Page_Builder $home_page_builder The front page builder for creating missing indexables.
127 * @param Indexable_Post_Type_Archive_Builder $post_type_archive_builder The post type archive builder for creating missing indexables.
128 * @param Indexable_Date_Archive_Builder $date_archive_builder The date archive builder for creating missing indexables.
129 * @param Indexable_System_Page_Builder $system_page_builder The search result builder for creating missing indexables.
130 * @param Indexable_Hierarchy_Builder $hierarchy_builder The hierarchy builder for creating the indexable hierarchy.
131 * @param Primary_Term_Builder $primary_term_builder The primary term builder for creating primary terms for posts.
132 * @param Indexable_Helper $indexable_helper The indexable helper.
133 * @param Indexable_Version_Manager $version_manager The indexable version manager.
134 * @param Indexable_Link_Builder $link_builder The link builder for creating missing SEO links.
135 * @param Logger $logger The logger.
136 */
137 public function __construct(
138 Indexable_Author_Builder $author_builder,
139 Indexable_Post_Builder $post_builder,
140 Indexable_Term_Builder $term_builder,
141 Indexable_Home_Page_Builder $home_page_builder,
142 Indexable_Post_Type_Archive_Builder $post_type_archive_builder,
143 Indexable_Date_Archive_Builder $date_archive_builder,
144 Indexable_System_Page_Builder $system_page_builder,
145 Indexable_Hierarchy_Builder $hierarchy_builder,
146 Primary_Term_Builder $primary_term_builder,
147 Indexable_Helper $indexable_helper,
148 Indexable_Version_Manager $version_manager,
149 Indexable_Link_Builder $link_builder,
150 Logger $logger
151 ) {
152 $this->author_builder = $author_builder;
153 $this->post_builder = $post_builder;
154 $this->term_builder = $term_builder;
155 $this->home_page_builder = $home_page_builder;
156 $this->post_type_archive_builder = $post_type_archive_builder;
157 $this->date_archive_builder = $date_archive_builder;
158 $this->system_page_builder = $system_page_builder;
159 $this->hierarchy_builder = $hierarchy_builder;
160 $this->primary_term_builder = $primary_term_builder;
161 $this->indexable_helper = $indexable_helper;
162 $this->version_manager = $version_manager;
163 $this->link_builder = $link_builder;
164 $this->logger = $logger;
165 }
166
167 /**
168 * Sets the indexable repository. Done to avoid circular dependencies.
169 *
170 * @required
171 *
172 * @param Indexable_Repository $indexable_repository The indexable repository.
173 *
174 * @return void
175 */
176 public function set_indexable_repository( Indexable_Repository $indexable_repository ) {
177 $this->indexable_repository = $indexable_repository;
178 }
179
180 /**
181 * Creates a clean copy of an Indexable to allow for later database operations.
182 *
183 * @param Indexable $indexable The Indexable to copy.
184 *
185 * @return bool|Indexable
186 */
187 protected function deep_copy_indexable( $indexable ) {
188 return $this->indexable_repository
189 ->query()
190 ->create( $indexable->as_array() );
191 }
192
193 /**
194 * Creates an indexable by its ID and type.
195 *
196 * @param int $object_id The indexable object ID.
197 * @param string $object_type The indexable object type.
198 * @param Indexable|bool $indexable Optional. An existing indexable to overwrite.
199 *
200 * @return bool|Indexable Instance of indexable. False when unable to build.
201 */
202 public function build_for_id_and_type( $object_id, $object_type, $indexable = false ) {
203 $defaults = [
204 'object_type' => $object_type,
205 'object_id' => $object_id,
206 ];
207
208 $indexable = $this->build( $indexable, $defaults );
209
210 return $indexable;
211 }
212
213 /**
214 * Creates an indexable for the homepage.
215 *
216 * @param Indexable|bool $indexable Optional. An existing indexable to overwrite.
217 *
218 * @return Indexable The home page indexable.
219 */
220 public function build_for_home_page( $indexable = false ) {
221 return $this->build( $indexable, [ 'object_type' => 'home-page' ] );
222 }
223
224 /**
225 * Creates an indexable for the date archive.
226 *
227 * @param Indexable|bool $indexable Optional. An existing indexable to overwrite.
228 *
229 * @return Indexable The date archive indexable.
230 */
231 public function build_for_date_archive( $indexable = false ) {
232 return $this->build( $indexable, [ 'object_type' => 'date-archive' ] );
233 }
234
235 /**
236 * Creates an indexable for a post type archive.
237 *
238 * @param string $post_type The post type.
239 * @param Indexable|bool $indexable Optional. An existing indexable to overwrite.
240 *
241 * @return Indexable The post type archive indexable.
242 */
243 public function build_for_post_type_archive( $post_type, $indexable = false ) {
244 $defaults = [
245 'object_type' => 'post-type-archive',
246 'object_sub_type' => $post_type,
247 ];
248 return $this->build( $indexable, $defaults );
249 }
250
251 /**
252 * Creates an indexable for a system page.
253 *
254 * @param string $page_type The type of system page.
255 * @param Indexable|bool $indexable Optional. An existing indexable to overwrite.
256 *
257 * @return Indexable The search result indexable.
258 */
259 public function build_for_system_page( $page_type, $indexable = false ) {
260 $defaults = [
261 'object_type' => 'system-page',
262 'object_sub_type' => $page_type,
263 ];
264 return $this->build( $indexable, $defaults );
265 }
266
267 /**
268 * Ensures we have a valid indexable. Creates one if false is passed.
269 *
270 * @param Indexable|false $indexable The indexable.
271 * @param array<string, int|string> $defaults The initial properties of the Indexable.
272 *
273 * @return Indexable The indexable.
274 */
275 protected function ensure_indexable( $indexable, $defaults = [] ) {
276 if ( ! $indexable ) {
277 return $this->indexable_repository->query()->create( $defaults );
278 }
279
280 return $indexable;
281 }
282
283 /**
284 * Build and author indexable from an author id if it does not exist yet, or if the author indexable needs to be upgraded.
285 *
286 * @param int $author_id The author id.
287 *
288 * @return Indexable|false The author indexable if it has been built, `false` if it could not be built.
289 */
290 protected function maybe_build_author_indexable( $author_id ) {
291 $author_indexable = $this->indexable_repository->find_by_id_and_type(
292 $author_id,
293 'user',
294 false,
295 );
296 if ( ! $author_indexable || $this->version_manager->indexable_needs_upgrade( $author_indexable ) ) {
297 // Try to build the author.
298 $author_defaults = [
299 'object_type' => 'user',
300 'object_id' => $author_id,
301 ];
302 $author_indexable = $this->build( $author_indexable, $author_defaults );
303 }
304 return $author_indexable;
305 }
306
307 /**
308 * Checks if the indexable type is one that is not supposed to have object ID for.
309 *
310 * @param string $type The type of the indexable.
311 *
312 * @return bool Whether the indexable type is one that is not supposed to have object ID for.
313 */
314 protected function is_type_with_no_id( $type ) {
315 return \in_array( $type, [ 'home-page', 'date-archive', 'post-type-archive', 'system-page' ], true );
316 }
317
318 // phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.Missing -- Most exceptions are handled in the method; the unexpected-error catch deliberately re-throws after firing the failure hook.
319
320 /**
321 * Rebuilds an Indexable from scratch.
322 *
323 * @param Indexable $indexable The Indexable to (re)build.
324 * @param array<string, int|string>|null $defaults The object type of the Indexable.
325 *
326 * @return Indexable|false The resulting Indexable.
327 */
328 public function build( $indexable, $defaults = null ) {
329 // Backup the previous Indexable, if there was one.
330 $indexable_before = ( $indexable ) ? $this->deep_copy_indexable( $indexable ) : null;
331
332 // Make sure we have an Indexable to work with.
333 $indexable = $this->ensure_indexable( $indexable, $defaults );
334
335 try {
336 if ( $indexable->object_id === 0 ) {
337 throw Not_Built_Exception::invalid_object_id( $indexable->object_id );
338 }
339 switch ( $indexable->object_type ) {
340
341 case 'post':
342 $indexable = $this->post_builder->build( $indexable->object_id, $indexable );
343
344 // Save indexable, to make sure it can be queried when building related objects like the author indexable and hierarchy.
345 $indexable = $this->indexable_helper->save_indexable( $indexable, $indexable_before );
346
347 // For attachments, we have to make sure to patch any potentially previously cleaned up SEO links.
348 if ( \is_a( $indexable, Indexable::class ) && $indexable->object_sub_type === 'attachment' ) {
349 $this->link_builder->patch_seo_links( $indexable );
350 }
351
352 // Always rebuild the primary term.
353 $this->primary_term_builder->build( $indexable->object_id );
354
355 // Always rebuild the hierarchy; this needs the primary term to run correctly.
356 $this->hierarchy_builder->build( $indexable );
357
358 $this->maybe_build_author_indexable( $indexable->author_id );
359
360 // The indexable is already saved, so return early.
361 return $indexable;
362
363 case 'user':
364 $indexable = $this->author_builder->build( $indexable->object_id, $indexable );
365 break;
366
367 case 'term':
368 $indexable = $this->term_builder->build( $indexable->object_id, $indexable );
369
370 // Save indexable, to make sure it can be queried when building hierarchy.
371 $indexable = $this->indexable_helper->save_indexable( $indexable, $indexable_before );
372
373 $this->hierarchy_builder->build( $indexable );
374
375 // The indexable is already saved, so return early.
376 return $indexable;
377
378 case 'home-page':
379 $indexable = $this->home_page_builder->build( $indexable );
380 break;
381
382 case 'date-archive':
383 $indexable = $this->date_archive_builder->build( $indexable );
384 break;
385
386 case 'post-type-archive':
387 $indexable = $this->post_type_archive_builder->build( $indexable->object_sub_type, $indexable );
388 break;
389
390 case 'system-page':
391 $indexable = $this->system_page_builder->build( $indexable->object_sub_type, $indexable );
392 break;
393 }
394
395 return $this->indexable_helper->save_indexable( $indexable, $indexable_before );
396 } catch ( Source_Exception $exception ) {
397 if ( ! $this->is_type_with_no_id( $indexable->object_type ) && ! isset( $indexable->object_id ) ) {
398 return false;
399 }
400
401 /**
402 * The current indexable could not be indexed. Create a placeholder indexable, so we can
403 * skip this indexable in future indexing runs.
404 *
405 * @var Indexable $indexable
406 */
407 $indexable = $this->ensure_indexable(
408 $indexable,
409 [
410 'object_id' => $indexable->object_id,
411 'object_type' => $indexable->object_type,
412 'post_status' => 'unindexed',
413 'version' => 0,
414 ],
415 );
416 // If we already had an existing indexable, mark it as unindexed. We cannot rely on its validity anymore.
417 $indexable->post_status = 'unindexed';
418 // Make sure that the indexing process doesn't get stuck in a loop on this broken indexable.
419 $indexable = $this->version_manager->set_latest( $indexable );
420
421 return $this->indexable_helper->save_indexable( $indexable, $indexable_before );
422 } catch ( Not_Built_Exception $exception ) {
423 $this->logger->debug(
424 $exception->getMessage(),
425 [
426 'object_id' => $indexable->object_id,
427 'object_type' => $indexable->object_type,
428 'object_sub_type' => $indexable->object_sub_type,
429 'exception' => \get_class( $exception ),
430 ],
431 );
432
433 return false;
434 } catch ( Indexing_Failed_Exception $exception ) {
435 // A nested build (e.g. the author indexable built during a post build) already logged the
436 // failure, fired the action and wrapped the original error, so pass it through untouched
437 // to keep the root failing object's identity and avoid reporting the failure twice.
438 throw $exception;
439 } catch ( Throwable $exception ) {
440 $indexing_failed_exception = new Indexing_Failed_Exception(
441 $indexable->object_id,
442 $indexable->object_type,
443 $indexable->object_sub_type,
444 $exception,
445 );
446
447 $this->logger->error(
448 $indexing_failed_exception->getMessage(),
449 [
450 'object_id' => $indexable->object_id,
451 'object_type' => $indexable->object_type,
452 'object_sub_type' => $indexable->object_sub_type,
453 'exception' => \get_class( $exception ),
454 ],
455 );
456
457 /**
458 * Fires when an indexable could not be built because of an unexpected error.
459 *
460 * This action lets third parties observe build failures themselves.
461 *
462 * @param int|null $object_id The object ID of the indexable that failed to build, or null for id-less object types.
463 * @param string $object_type The object type of the indexable that failed to build.
464 * @param string|null $object_sub_type The object sub type of the indexable that failed to build.
465 * @param Throwable $exception The error that caused the failure.
466 */
467 \do_action(
468 'wpseo_indexable_indexing_failed',
469 $indexable->object_id,
470 $indexable->object_type,
471 $indexable->object_sub_type,
472 $exception,
473 );
474
475 throw $indexing_failed_exception;
476 }
477 }
478
479 // phpcs:enable
480 }
481