PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 27.5
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v27.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
wordpress-seo / src / repositories / indexable-repository.php

indexable-repository.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 27.5, at src/repositories/indexable-repository.php

725 lines 21.6 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\Repositories;
4
5 use Psr\Log\LoggerInterface;
6 use wpdb;
7 use Yoast\WP\Lib\Model;
8 use Yoast\WP\Lib\ORM;
9 use Yoast\WP\SEO\Builders\Indexable_Builder;
10 use Yoast\WP\SEO\Helpers\Current_Page_Helper;
11 use Yoast\WP\SEO\Helpers\Indexable_Helper;
12 use Yoast\WP\SEO\Loggers\Logger;
13 use Yoast\WP\SEO\Models\Indexable;
14 use Yoast\WP\SEO\Services\Indexables\Indexable_Version_Manager;
15
16 /**
17 * Class Indexable_Repository.
18 */
19 class Indexable_Repository {
20
21 /**
22 * The indexable builder.
23 *
24 * @var Indexable_Builder
25 */
26 private $builder;
27
28 /**
29 * Represents the hierarchy repository.
30 *
31 * @var Indexable_Hierarchy_Repository
32 */
33 protected $hierarchy_repository;
34
35 /**
36 * The current page helper.
37 *
38 * @var Current_Page_Helper
39 */
40 protected $current_page;
41
42 /**
43 * The logger object.
44 *
45 * @var LoggerInterface
46 */
47 protected $logger;
48
49 /**
50 * The WordPress database.
51 *
52 * @var wpdb
53 */
54 protected $wpdb;
55
56 /**
57 * Represents the indexable helper.
58 *
59 * @var Indexable_Helper
60 */
61 protected $indexable_helper;
62
63 /**
64 * Checks if Indexables are up to date.
65 *
66 * @var Indexable_Version_Manager
67 */
68 protected $version_manager;
69
70 /**
71 * Returns the instance of this class constructed through the ORM Wrapper.
72 *
73 * @param Indexable_Builder $builder The indexable builder.
74 * @param Current_Page_Helper $current_page The current post helper.
75 * @param Logger $logger The logger.
76 * @param Indexable_Hierarchy_Repository $hierarchy_repository The hierarchy repository.
77 * @param wpdb $wpdb The WordPress database instance.
78 * @param Indexable_Version_Manager $version_manager The indexable version manager.
79 */
80 public function __construct(
81 Indexable_Builder $builder,
82 Current_Page_Helper $current_page,
83 Logger $logger,
84 Indexable_Hierarchy_Repository $hierarchy_repository,
85 wpdb $wpdb,
86 Indexable_Version_Manager $version_manager
87 ) {
88 $this->builder = $builder;
89 $this->current_page = $current_page;
90 $this->logger = $logger;
91 $this->hierarchy_repository = $hierarchy_repository;
92 $this->wpdb = $wpdb;
93 $this->version_manager = $version_manager;
94 }
95
96 /**
97 * Starts a query for this repository.
98 *
99 * @return ORM
100 */
101 public function query() {
102 return Model::of_type( 'Indexable' );
103 }
104
105 /**
106 * Attempts to find the indexable for the current WordPress page. Returns false if no indexable could be found.
107 * This may be the result of the indexable not existing or of being unable to determine what type of page the
108 * current page is.
109 *
110 * @return bool|Indexable The indexable. If no indexable is found returns an empty indexable. Returns false if
111 * there is a database error.
112 */
113 public function for_current_page() {
114 $indexable = false;
115
116 switch ( true ) {
117 case $this->current_page->is_simple_page():
118 $indexable = $this->find_by_id_and_type( $this->current_page->get_simple_page_id(), 'post' );
119 break;
120 case $this->current_page->is_home_static_page():
121 $indexable = $this->find_by_id_and_type( $this->current_page->get_front_page_id(), 'post' );
122 break;
123 case $this->current_page->is_home_posts_page():
124 $indexable = $this->find_for_home_page();
125 break;
126 case $this->current_page->is_term_archive():
127 $indexable = $this->find_by_id_and_type( $this->current_page->get_term_id(), 'term' );
128 break;
129 case $this->current_page->is_date_archive():
130 $indexable = $this->find_for_date_archive();
131 break;
132 case $this->current_page->is_search_result():
133 $indexable = $this->find_for_system_page( 'search-result' );
134 break;
135 case $this->current_page->is_post_type_archive():
136 $indexable = $this->find_for_post_type_archive( $this->current_page->get_queried_post_type() );
137 break;
138 case $this->current_page->is_author_archive():
139 $indexable = $this->find_by_id_and_type( $this->current_page->get_author_id(), 'user' );
140 break;
141 case $this->current_page->is_404():
142 $indexable = $this->find_for_system_page( '404' );
143 break;
144 }
145
146 if ( $indexable === false ) {
147 return $this->query()->create(
148 [
149 'object_type' => 'unknown',
150 'post_status' => 'unindexed',
151 'version' => 1,
152 ],
153 );
154 }
155
156 return $indexable;
157 }
158
159 /**
160 * Retrieves an indexable by its permalink.
161 *
162 * @param string $permalink The indexable permalink.
163 *
164 * @return bool|Indexable The indexable, false if none could be found.
165 */
166 public function find_by_permalink( $permalink ) {
167 $permalink_hash = \strlen( $permalink ) . ':' . \md5( $permalink );
168
169 // Find by both permalink_hash and permalink, permalink_hash is indexed so will be used first by the DB to optimize the query.
170 return $this->query()
171 ->where( 'permalink_hash', $permalink_hash )
172 ->where( 'permalink', $permalink )
173 ->find_one();
174 }
175
176 /**
177 * Retrieves all the indexable instances of a certain object type.
178 *
179 * @param string $object_type The object type.
180 *
181 * @return Indexable[] The array with all the indexable instances of a certain object type.
182 */
183 public function find_all_with_type( $object_type ) {
184 /**
185 * The array with all the indexable instances of a certain object type.
186 *
187 * @var Indexable[] $indexables
188 */
189 $indexables = $this
190 ->query()
191 ->where( 'object_type', $object_type )
192 ->find_many();
193
194 return \array_map( [ $this, 'upgrade_indexable' ], $indexables );
195 }
196
197 /**
198 * Retrieves all the indexable instances of a certain object subtype.
199 *
200 * @param string $object_type The object type.
201 * @param string $object_sub_type The object subtype.
202 *
203 * @return Indexable[] The array with all the indexable instances of a certain object subtype.
204 */
205 public function find_all_with_type_and_sub_type( $object_type, $object_sub_type ) {
206 /**
207 * The array with all the indexable instances of a certain object type and subtype.
208 *
209 * @var Indexable[] $indexables
210 */
211 $indexables = $this
212 ->query()
213 ->where( 'object_type', $object_type )
214 ->where( 'object_sub_type', $object_sub_type )
215 ->find_many();
216
217 return \array_map( [ $this, 'upgrade_indexable' ], $indexables );
218 }
219
220 /**
221 * Retrieves a paginated set of indexable instances of public indexables.
222 *
223 * @param int $page The page number (1-based).
224 * @param int $page_size The number of items per page.
225 * @param string $post_type The post type indexables to find.
226 *
227 * @return Indexable[] The array with the paginated indexable instances which are public.
228 */
229 public function find_all_public_paginated( int $page, int $page_size, string $post_type ): array {
230 $offset = ( ( $page - 1 ) * $page_size );
231
232 $query = $this->query()->where_raw( '( is_public IS NULL OR is_public = 1 ) AND ( is_robots_noindex IS NULL OR is_robots_noindex = 0 )' );
233 $query->where( 'object_sub_type', $post_type );
234 $query->where( 'post_status', 'publish' );
235
236 $indexables = $query->order_by_asc( 'id' )->limit( $page_size )->offset( $offset )->find_many();
237
238 return \array_map( [ $this, 'upgrade_indexable' ], $indexables );
239 }
240
241 /**
242 * Retrieves the homepage indexable.
243 *
244 * @param bool $auto_create Optional. Create the indexable if it does not exist.
245 *
246 * @return bool|Indexable Instance of indexable.
247 */
248 public function find_for_home_page( $auto_create = true ) {
249 $indexable = \wp_cache_get( 'home-page', 'yoast-seo-indexables' );
250 if ( ! $indexable ) {
251 /**
252 * Indexable instance.
253 *
254 * @var Indexable $indexable
255 */
256 $indexable = $this->query()->where( 'object_type', 'home-page' )->find_one();
257
258 if ( $auto_create && ! $indexable ) {
259 $indexable = $this->builder->build_for_home_page();
260 }
261
262 $indexable = $this->upgrade_indexable( $indexable );
263
264 \wp_cache_set( 'home-page', $indexable, 'yoast-seo-indexables', ( 5 * \MINUTE_IN_SECONDS ) );
265 }
266
267 return $indexable;
268 }
269
270 /**
271 * Retrieves the date archive indexable.
272 *
273 * @param bool $auto_create Optional. Create the indexable if it does not exist.
274 *
275 * @return bool|Indexable Instance of indexable.
276 */
277 public function find_for_date_archive( $auto_create = true ) {
278 /**
279 * Indexable instance.
280 *
281 * @var Indexable $indexable
282 */
283 $indexable = $this->query()->where( 'object_type', 'date-archive' )->find_one();
284
285 if ( $auto_create && ! $indexable ) {
286 $indexable = $this->builder->build_for_date_archive();
287 }
288
289 return $this->upgrade_indexable( $indexable );
290 }
291
292 /**
293 * Retrieves an indexable for a post type archive.
294 *
295 * @param string $post_type The post type.
296 * @param bool $auto_create Optional. Create the indexable if it does not exist.
297 *
298 * @return bool|Indexable The indexable, false if none could be found.
299 */
300 public function find_for_post_type_archive( $post_type, $auto_create = true ) {
301 /**
302 * Indexable instance.
303 *
304 * @var Indexable $indexable
305 */
306 $indexable = $this->query()
307 ->where( 'object_type', 'post-type-archive' )
308 ->where( 'object_sub_type', $post_type )
309 ->find_one();
310
311 if ( $auto_create && ! $indexable ) {
312 $indexable = $this->builder->build_for_post_type_archive( $post_type );
313 }
314
315 return $this->upgrade_indexable( $indexable );
316 }
317
318 /**
319 * Retrieves the indexable for a system page.
320 *
321 * @param string $object_sub_type The type of system page.
322 * @param bool $auto_create Optional. Create the indexable if it does not exist.
323 *
324 * @return bool|Indexable Instance of indexable.
325 */
326 public function find_for_system_page( $object_sub_type, $auto_create = true ) {
327 /**
328 * Indexable instance.
329 *
330 * @var Indexable $indexable
331 */
332 $indexable = $this->query()
333 ->where( 'object_type', 'system-page' )
334 ->where( 'object_sub_type', $object_sub_type )
335 ->find_one();
336
337 if ( $auto_create && ! $indexable ) {
338 $indexable = $this->builder->build_for_system_page( $object_sub_type );
339 }
340
341 return $this->upgrade_indexable( $indexable );
342 }
343
344 /**
345 * Retrieves an indexable by its ID and type.
346 *
347 * @param int $object_id The indexable object ID.
348 * @param string $object_type The indexable object type.
349 * @param bool $auto_create Optional. Create the indexable if it does not exist.
350 *
351 * @return bool|Indexable Instance of indexable.
352 */
353 public function find_by_id_and_type( $object_id, $object_type, $auto_create = true ) {
354 $indexable = $this->query()
355 ->where( 'object_id', $object_id )
356 ->where( 'object_type', $object_type )
357 ->find_one();
358
359 if ( $auto_create && ! $indexable ) {
360 $indexable = $this->builder->build_for_id_and_type( $object_id, $object_type );
361 }
362 else {
363 $indexable = $this->upgrade_indexable( $indexable );
364 }
365
366 return $indexable;
367 }
368
369 /**
370 * Retrieves multiple indexables at once by their id's and type.
371 *
372 * @param int[] $object_ids The array of indexable object id's.
373 * @param string $object_type The indexable object type.
374 * @param bool $auto_create Optional. Create the indexable if it does not exist.
375 *
376 * @return Indexable[] An array of indexables.
377 */
378 public function find_by_multiple_ids_and_type( $object_ids, $object_type, $auto_create = true ) {
379 if ( empty( $object_ids ) ) {
380 return [];
381 }
382
383 /**
384 * Represents an array of indexable objects.
385 *
386 * @var Indexable[] $indexables
387 */
388 $indexables = $this->query()
389 ->where_in( 'object_id', $object_ids )
390 ->where( 'object_type', $object_type )
391 ->find_many();
392
393 if ( $auto_create ) {
394 $indexables_available = [];
395 foreach ( $indexables as $indexable ) {
396 $indexables_available[] = $indexable->object_id;
397 }
398
399 $indexables_to_create = \array_diff( $object_ids, $indexables_available );
400
401 foreach ( $indexables_to_create as $indexable_to_create ) {
402 $indexables[] = $this->builder->build_for_id_and_type( $indexable_to_create, $object_type );
403 }
404 }
405
406 return \array_map( [ $this, 'upgrade_indexable' ], $indexables );
407 }
408
409 /**
410 * Finds the indexables by id's.
411 *
412 * @param array $indexable_ids The indexable id's.
413 *
414 * @return Indexable[] The found indexables.
415 */
416 public function find_by_ids( array $indexable_ids ) {
417 if ( empty( $indexable_ids ) ) {
418 return [];
419 }
420
421 $indexables = $this
422 ->query()
423 ->where_in( 'id', $indexable_ids )
424 ->find_many();
425
426 return \array_map( [ $this, 'upgrade_indexable' ], $indexables );
427 }
428
429 /**
430 * Returns all ancestors of a given indexable.
431 *
432 * @param Indexable $indexable The indexable to find the ancestors of.
433 *
434 * @return Indexable[] All ancestors of the given indexable.
435 */
436 public function get_ancestors( Indexable $indexable ) {
437 // If we've already set ancestors on the indexable no need to get them again.
438 if ( \is_array( $indexable->ancestors ) && ! empty( $indexable->ancestors ) ) {
439 return \array_map( [ $this, 'upgrade_indexable' ], $indexable->ancestors );
440 }
441
442 $indexable_ids = $this->hierarchy_repository->find_ancestors( $indexable );
443
444 // If we've set ancestors on the indexable because we had to build them to find them.
445 if ( \is_array( $indexable->ancestors ) && ! empty( $indexable->ancestors ) ) {
446 return \array_map( [ $this, 'upgrade_indexable' ], $indexable->ancestors );
447 }
448
449 if ( empty( $indexable_ids ) ) {
450 return [];
451 }
452
453 if ( $indexable_ids[0] === 0 && \count( $indexable_ids ) === 1 ) {
454 return [];
455 }
456
457 $indexables = $this->query()
458 ->where_in( 'id', $indexable_ids )
459 ->order_by_expr( 'FIELD(id,' . \implode( ',', $indexable_ids ) . ')' )
460 ->find_many();
461
462 return \array_map( [ $this, 'upgrade_indexable' ], $indexables );
463 }
464
465 /**
466 * Returns all subpages with a given post_parent.
467 *
468 * @param int $post_parent The post parent.
469 * @param array $exclude_ids The id's to exclude.
470 *
471 * @return Indexable[] array of indexables.
472 */
473 public function get_subpages_by_post_parent( $post_parent, $exclude_ids = [] ) {
474 $query = $this->query()
475 ->where( 'post_parent', $post_parent )
476 ->where( 'object_type', 'post' )
477 ->where( 'post_status', 'publish' );
478
479 if ( ! empty( $exclude_ids ) ) {
480 $query->where_not_in( 'object_id', $exclude_ids );
481 }
482 return $query->find_many();
483 }
484
485 /**
486 * Returns most recently modified posts of a post type.
487 *
488 * @param string $post_type The post type.
489 * @param int $limit The maximum number of posts to return.
490 * @param bool $exclude_older_than_one_year Whether to exclude posts older than one year.
491 * @param string $search_filter Optional. A search filter to apply to the breadcrumb title.
492 *
493 * @return Indexable[] array of indexables.
494 */
495 public function get_recently_modified_posts( string $post_type, int $limit, bool $exclude_older_than_one_year, string $search_filter = '' ) {
496 $query = $this->query()
497 ->where( 'object_type', 'post' )
498 ->where( 'object_sub_type', $post_type )
499 ->where_raw( '( is_public IS NULL OR is_public = 1 )' )
500 ->order_by_desc( 'object_last_modified' )
501 ->limit( $limit );
502
503 if ( $exclude_older_than_one_year === true ) {
504 $query->where_gte( 'object_published_at', \gmdate( 'Y-m-d H:i:s', \strtotime( '-1 year' ) ) );
505 }
506
507 if ( $search_filter !== '' ) {
508 $query->where_like( 'breadcrumb_title', '%' . $search_filter . '%' );
509 }
510
511 $query->order_by_desc( 'object_last_modified' )
512 ->limit( $limit );
513
514 return $query->find_many();
515 }
516
517 /**
518 * Returns the most recently modified cornerstone content of a post type.
519 *
520 * @param string $post_type The post type.
521 * @param int|null $limit The maximum number of posts to return.
522 *
523 * @return Indexable[] array of indexables.
524 */
525 public function get_recent_cornerstone_for_post_type( string $post_type, ?int $limit ) {
526 $query = $this->query()
527 ->where( 'object_type', 'post' )
528 ->where( 'object_sub_type', $post_type )
529 ->where_raw( '( is_public IS NULL OR is_public = 1 )' )
530 ->where( 'is_cornerstone', 1 )
531 ->order_by_desc( 'object_last_modified' );
532
533 if ( $limit !== null ) {
534 $query->limit( $limit );
535 }
536
537 return $query->find_many();
538 }
539
540 /**
541 * Returns the most recently modified posts with keywords of a post type.
542 *
543 * @param string $post_type The post type.
544 * @param int|null $limit The maximum number of posts to return.
545 * @param string|null $date_limit Only include content modified after this date.
546 *
547 * @return array<array<string, string>>|false The array of indexable columns. False if the query failed.
548 */
549 public function get_recent_posts_with_keywords_for_post_type( string $post_type, ?int $limit = null, ?string $date_limit = null ) {
550 $query = $this->query()
551 ->select( 'object_id' )
552 ->select( 'primary_focus_keyword_score' )
553 ->select( 'breadcrumb_title' )
554 ->where( 'object_type', 'post' )
555 ->where( 'object_sub_type', $post_type )
556 ->where_not_equal( 'primary_focus_keyword_score', 0 )
557 ->where_not_null( 'primary_focus_keyword_score' )
558 ->where_raw( "( post_status = 'publish' OR post_status IS NULL )" )
559 ->where_raw( '( is_robots_noindex IS NULL OR is_robots_noindex <> 1 )' )
560 ->order_by_desc( 'object_last_modified' );
561
562 if ( $limit !== null ) {
563 $query->limit( $limit );
564 }
565
566 if ( $date_limit !== null ) {
567 $query->where_gte( 'object_last_modified', $date_limit );
568 }
569
570 return $query->find_array();
571 }
572
573 /**
574 * Returns the most recently modified posts with readability scores of a post type.
575 *
576 * @param string $post_type The post type.
577 * @param int|null $limit The maximum number of posts to return.
578 * @param string|null $date_limit Only include content modified after this date.
579 *
580 * @return array<array<string, string|null>>|false The array of indexable columns. False if the query failed.
581 */
582 public function get_recent_posts_with_readability_scores_for_post_type( string $post_type, ?int $limit = null, ?string $date_limit = null ) {
583 $query = $this->query()
584 ->select( 'object_id' )
585 ->select( 'readability_score' )
586 ->select( 'breadcrumb_title' )
587 ->where( 'object_type', 'post' )
588 ->where( 'object_sub_type', $post_type )
589 ->where_not_null( 'estimated_reading_time_minutes' )
590 ->where_raw( "( post_status = 'publish' OR post_status IS NULL )" )
591 ->order_by_desc( 'object_last_modified' );
592
593 if ( $limit !== null ) {
594 $query->limit( $limit );
595 }
596
597 if ( $date_limit !== null ) {
598 $query->where_gte( 'object_last_modified', $date_limit );
599 }
600
601 return $query->find_array();
602 }
603
604 /**
605 * Returns the most recently modified posts for a post type.
606 *
607 * @param string $post_type The post type.
608 * @param int|null $limit The maximum number of posts to return.
609 * @param string|null $date_limit Only include content modified after this date.
610 *
611 * @return array<array<string, string|null>>|false The array of indexable columns. False if the query failed.
612 */
613 public function get_recent_posts_for_post_type( string $post_type, ?int $limit = null, ?string $date_limit = null ) {
614 $query = $this->query()
615 ->select( 'object_id' )
616 ->select( 'breadcrumb_title' )
617 ->select( 'description' )
618 ->where( 'object_type', 'post' )
619 ->where( 'object_sub_type', $post_type )
620 ->where_raw( "( post_status = 'publish' OR post_status IS NULL )" )
621 ->where_raw( '( is_robots_noindex IS NULL OR is_robots_noindex <> 1 )' )
622 ->order_by_desc( 'object_last_modified' );
623
624 if ( $limit !== null ) {
625 $query->limit( $limit );
626 }
627
628 if ( $date_limit !== null ) {
629 $query->where_gte( 'object_last_modified', $date_limit );
630 }
631
632 return $query->find_array();
633 }
634
635 /**
636 * Updates the incoming link count for an indexable without first fetching it.
637 *
638 * @param int $indexable_id The indexable id.
639 * @param int $count The incoming link count.
640 *
641 * @return bool Whether or not the update was succeful.
642 */
643 public function update_incoming_link_count( $indexable_id, $count ) {
644 return (bool) $this->query()
645 ->set( 'incoming_link_count', $count )
646 ->where( 'id', $indexable_id )
647 ->update_many();
648 }
649
650 /**
651 * Ensures that the given indexable has a permalink.
652 *
653 * Will be deprecated in 17.3 - Use upgrade_indexable instead.
654 *
655 * @codeCoverageIgnore
656 *
657 * @param Indexable $indexable The indexable.
658 *
659 * @return bool|Indexable The indexable.
660 */
661 public function ensure_permalink( $indexable ) {
662 // @phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- self::class is safe.
663 // @phpcs:ignore Squiz.PHP.CommentedOutCode.Found
664 // _deprecated_function( __METHOD__, 'Yoast SEO 17.3', self::class . '::upgrade_indexable' );
665
666 return $this->upgrade_indexable( $indexable );
667 }
668
669 /**
670 * Checks if an Indexable is outdated, and rebuilds it when necessary.
671 *
672 * @param Indexable $indexable The indexable.
673 *
674 * @return Indexable The indexable.
675 */
676 public function upgrade_indexable( $indexable ) {
677 if ( $this->version_manager->indexable_needs_upgrade( $indexable ) ) {
678 $indexable = $this->builder->build( $indexable );
679 }
680 return $indexable;
681 }
682
683 /**
684 * Resets the permalinks of the passed object type and subtype.
685 *
686 * @param string|null $type The type of the indexable. Can be null.
687 * @param string|null $subtype The subtype. Can be null.
688 * @param int|null $object_id The object ID. Can be null.
689 *
690 * @return int|bool The number of permalinks changed if the query was succesful. False otherwise.
691 */
692 public function reset_permalink( $type = null, $subtype = null, $object_id = null ) {
693 $query = $this->query()->set(
694 [
695 'permalink' => null,
696 'permalink_hash' => null,
697 'version' => 0,
698 ],
699 );
700
701 if ( $type !== null ) {
702 $query->where( 'object_type', $type );
703 }
704
705 if ( $type !== null && $subtype !== null ) {
706 $query->where( 'object_sub_type', $subtype );
707 }
708
709 if ( $object_id !== null ) {
710 $query->where( 'object_id', $object_id );
711 }
712
713 return $query->update_many();
714 }
715
716 /**
717 * Gets the total number of stored indexables.
718 *
719 * @return int The total number of stored indexables.
720 */
721 public function get_total_number_of_indexables() {
722 return $this->query()->count();
723 }
724 }
725