PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.5
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.5
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/builders/indexable-builder.php +156 -75 18.028.5 View file →
@@ -1,10 +1,14 @@
1 1 <?php
2 2
3 3 namespace Yoast\WP\SEO\Builders;
4 4
5 +use Throwable;
6 +use Yoast\WP\SEO\Exceptions\Indexable\Indexing_Failed_Exception;
7 +use Yoast\WP\SEO\Exceptions\Indexable\Not_Built_Exception;
5 8 use Yoast\WP\SEO\Exceptions\Indexable\Source_Exception;
6 9 use Yoast\WP\SEO\Helpers\Indexable_Helper;
10 +use Yoast\WP\SEO\Loggers\Logger;
7 11 use Yoast\WP\SEO\Models\Indexable;
8 12 use Yoast\WP\SEO\Repositories\Indexable_Repository;
9 13 use Yoast\WP\SEO\Services\Indexables\Indexable_Version_Manager;
10 14
@@ -78,8 +82,15 @@
78 82 */
79 83 private $primary_term_builder;
80 84
81 85 /**
86 + * The link builder
87 + *
88 + * @var Indexable_Link_Builder
89 + */
90 + private $link_builder;
91 +
92 + /**
82 93 * The indexable repository.
83 94 *
84 95 * @var Indexable_Repository
85 96 */
@@ -99,8 +110,15 @@
99 110 */
100 111 protected $version_manager;
101 112
102 113 /**
114 + * The logger.
115 + *
116 + * @var Logger
117 + */
118 + protected $logger;
119 +
120 + /**
103 121 * Returns the instance of this class constructed through the ORM Wrapper.
104 122 *
105 123 * @param Indexable_Author_Builder $author_builder The author builder for creating missing indexables.
106 124 * @param Indexable_Post_Builder $post_builder The post builder for creating missing indexables.
@@ -112,8 +130,10 @@
112 130 * @param Indexable_Hierarchy_Builder $hierarchy_builder The hierarchy builder for creating the indexable hierarchy.
113 131 * @param Primary_Term_Builder $primary_term_builder The primary term builder for creating primary terms for posts.
114 132 * @param Indexable_Helper $indexable_helper The indexable helper.
115 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.
116 136 */
117 137 public function __construct(
118 138 Indexable_Author_Builder $author_builder,
119 139 Indexable_Post_Builder $post_builder,
@@ -124,9 +144,11 @@
124 144 Indexable_System_Page_Builder $system_page_builder,
125 145 Indexable_Hierarchy_Builder $hierarchy_builder,
126 146 Primary_Term_Builder $primary_term_builder,
127 147 Indexable_Helper $indexable_helper,
128 - Indexable_Version_Manager $version_manager
148 + Indexable_Version_Manager $version_manager,
149 + Indexable_Link_Builder $link_builder,
150 + Logger $logger
129 151 ) {
130 152 $this->author_builder = $author_builder;
131 153 $this->post_builder = $post_builder;
132 154 $this->term_builder = $term_builder;
@@ -137,8 +159,10 @@
137 159 $this->hierarchy_builder = $hierarchy_builder;
138 160 $this->primary_term_builder = $primary_term_builder;
139 161 $this->indexable_helper = $indexable_helper;
140 162 $this->version_manager = $version_manager;
163 + $this->link_builder = $link_builder;
164 + $this->logger = $logger;
141 165 }
142 166
143 167 /**
144 168 * Sets the indexable repository. Done to avoid circular dependencies.
@@ -145,8 +169,10 @@
145 169 *
146 170 * @required
147 171 *
148 172 * @param Indexable_Repository $indexable_repository The indexable repository.
173 + *
174 + * @return void
149 175 */
150 176 public function set_indexable_repository( Indexable_Repository $indexable_repository ) {
151 177 $this->indexable_repository = $indexable_repository;
152 178 }
@@ -240,14 +266,14 @@
240 266
241 267 /**
242 268 * Ensures we have a valid indexable. Creates one if false is passed.
243 269 *
244 - * @param Indexable|false $indexable The indexable.
245 - * @param array $defaults The initial properties of the Indexable.
270 + * @param Indexable|false $indexable The indexable.
271 + * @param array<string, int|string> $defaults The initial properties of the Indexable.
246 272 *
247 273 * @return Indexable The indexable.
248 274 */
249 - private function ensure_indexable( $indexable, $defaults = [] ) {
275 + protected function ensure_indexable( $indexable, $defaults = [] ) {
250 276 if ( ! $indexable ) {
251 277 return $this->indexable_repository->query()->create( $defaults );
252 278 }
253 279
@@ -254,57 +280,49 @@
254 280 return $indexable;
255 281 }
256 282
257 283 /**
258 - * Saves and returns an indexable (on production environments only).
284 + * Build and author indexable from an author id if it does not exist yet, or if the author indexable needs to be upgraded.
259 285 *
260 - * @param Indexable $indexable The indexable.
261 - * @param Indexable|null $indexable_before The indexable before possible changes.
286 + * @param int $author_id The author id.
262 287 *
263 - * @return Indexable The indexable.
288 + * @return Indexable|false The author indexable if it has been built, `false` if it could not be built.
264 289 */
265 - protected function save_indexable( $indexable, $indexable_before = null ) {
266 - $intend_to_save = $this->indexable_helper->should_index_indexables();
267 -
268 - /**
269 - * Filter: 'wpseo_should_save_indexable' - Allow developers to enable / disable
270 - * saving the indexable when the indexable is updated. Warning: overriding
271 - * the intended action may cause problems when moving from a staging to a
272 - * production environment because indexable permalinks may get set incorrectly.
273 - *
274 - * @param Indexable $indexable The indexable to be saved.
275 - *
276 - * @api bool $intend_to_save True if YoastSEO intends to save the indexable.
277 - */
278 - $intend_to_save = \apply_filters( 'wpseo_should_save_indexable', $intend_to_save, $indexable );
279 -
280 - if ( ! $intend_to_save ) {
281 - return $indexable;
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 );
282 303 }
304 + return $author_indexable;
305 + }
283 306
284 - // Save the indexable before running the WordPress hook.
285 - $indexable->save();
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 + }
286 317
287 - if ( $indexable_before ) {
288 - /**
289 - * Action: 'wpseo_save_indexable' - Allow developers to perform an action
290 - * when the indexable is updated.
291 - *
292 - * @param Indexable $indexable_before The indexable before saving.
293 - *
294 - * @api Indexable $indexable The saved indexable.
295 - */
296 - \do_action( 'wpseo_save_indexable', $indexable, $indexable_before );
297 - }
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.
298 319
299 - return $indexable;
300 - }
301 -
302 320 /**
303 321 * Rebuilds an Indexable from scratch.
304 322 *
305 - * @param Indexable $indexable The Indexable to (re)build.
306 - * @param array|null $defaults The object type of the Indexable.
323 + * @param Indexable $indexable The Indexable to (re)build.
324 + * @param array<string, int|string>|null $defaults The object type of the Indexable.
307 325 *
308 326 * @return Indexable|false The resulting Indexable.
309 327 */
310 328 public function build( $indexable, $defaults = null ) {
@@ -314,15 +332,22 @@
314 332 // Make sure we have an Indexable to work with.
315 333 $indexable = $this->ensure_indexable( $indexable, $defaults );
316 334
317 335 try {
336 + if ( $indexable->object_id === 0 ) {
337 + throw Not_Built_Exception::invalid_object_id( $indexable->object_id );
338 + }
318 339 switch ( $indexable->object_type ) {
319 340
320 341 case 'post':
321 342 $indexable = $this->post_builder->build( $indexable->object_id, $indexable );
322 - if ( ! $indexable ) {
323 - // Indexable for this Post was not built for a reason; e.g. if its post type is excluded.
324 - return $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 );
325 350 }
326 351
327 352 // Always rebuild the primary term.
328 353 $this->primary_term_builder->build( $indexable->object_id );
@@ -329,23 +354,13 @@
329 354
330 355 // Always rebuild the hierarchy; this needs the primary term to run correctly.
331 356 $this->hierarchy_builder->build( $indexable );
332 357
333 - // Rebuild the author indexable only when necessary.
334 - $author_indexable = $this->indexable_repository->find_by_id_and_type(
335 - $indexable->author_id,
336 - 'user',
337 - false
338 - );
339 - if ( ! $author_indexable || $this->version_manager->indexable_needs_upgrade( $author_indexable ) ) {
340 - $author_defaults = [
341 - 'object_type' => 'user',
342 - 'object_id' => $indexable->author_id,
343 - ];
344 - $this->build( $author_indexable, $author_defaults );
345 - }
346 - break;
358 + $this->maybe_build_author_indexable( $indexable->author_id );
347 359
360 + // The indexable is already saved, so return early.
361 + return $indexable;
362 +
348 363 case 'user':
349 364 $indexable = $this->author_builder->build( $indexable->object_id, $indexable );
350 365 break;
351 366
@@ -350,11 +365,17 @@
350 365 break;
351 366
352 367 case 'term':
353 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 +
354 373 $this->hierarchy_builder->build( $indexable );
355 - break;
356 374
375 + // The indexable is already saved, so return early.
376 + return $indexable;
377 +
357 378 case 'home-page':
358 379 $indexable = $this->home_page_builder->build( $indexable );
359 380 break;
360 381
@@ -370,11 +391,14 @@
370 391 $indexable = $this->system_page_builder->build( $indexable->object_sub_type, $indexable );
371 392 break;
372 393 }
373 394
374 - return $this->save_indexable( $indexable, $indexable_before );
375 - }
376 - catch ( Source_Exception $exception ) {
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 +
377 401 /**
378 402 * The current indexable could not be indexed. Create a placeholder indexable, so we can
379 403 * skip this indexable in future indexing runs.
380 404 *
@@ -379,21 +403,78 @@
379 403 * skip this indexable in future indexing runs.
380 404 *
381 405 * @var Indexable $indexable
382 406 */
383 - $indexable = $this->indexable_repository
384 - ->query()
385 - ->create(
386 - [
387 - 'object_id' => $indexable->object_id,
388 - 'object_type' => $indexable->object_type,
389 - 'post_status' => 'unindexed',
390 - 'version' => 0,
391 - ]
392 - );
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';
393 418 // Make sure that the indexing process doesn't get stuck in a loop on this broken indexable.
394 419 $indexable = $this->version_manager->set_latest( $indexable );
395 420
396 - return $this->save_indexable( $indexable, $indexable_before );
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;
397 476 }
398 477 }
478 +
479 + // phpcs:enable
399 480 }