PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / trunk
Yoast SEO – Advanced SEO with real-time guidance and built-in AI vtrunk
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/repositories/indexable-repository.php +298 -9 18.0 → trunk View file →
@@ -18,8 +18,23 @@
18 18 */
19 19 class Indexable_Repository {
20 20
21 21 /**
22 + * The maximum number of comma-separated phrases honoured by a title-keyword search.
23 + * Any phrases beyond this are ignored so an oversized list cannot blow up the query.
24 + *
25 + * @var int
26 + */
27 + public const MAX_TITLE_KEYWORD_PHRASES = 10;
28 +
29 + /**
30 + * The maximum page size honoured by a title-keyword search.
31 + *
32 + * @var int
33 + */
34 + public const MAX_TITLE_KEYWORD_PAGE_SIZE = 100;
35 +
36 + /**
22 37 * The indexable builder.
23 38 *
24 39 * @var Indexable_Builder
25 40 */
@@ -106,9 +121,10 @@
106 121 * Attempts to find the indexable for the current WordPress page. Returns false if no indexable could be found.
107 122 * This may be the result of the indexable not existing or of being unable to determine what type of page the
108 123 * current page is.
109 124 *
110 - * @return bool|Indexable The indexable, false if none could be found.
125 + * @return bool|Indexable The indexable. If no indexable is found returns an empty indexable. Returns false if
126 + * there is a database error.
111 127 */
112 128 public function for_current_page() {
113 129 $indexable = false;
114 130
@@ -147,9 +163,9 @@
147 163 [
148 164 'object_type' => 'unknown',
149 165 'post_status' => 'unindexed',
150 166 'version' => 1,
151 - ]
167 + ],
152 168 );
153 169 }
154 170
155 171 return $indexable;
@@ -216,8 +232,29 @@
216 232 return \array_map( [ $this, 'upgrade_indexable' ], $indexables );
217 233 }
218 234
219 235 /**
236 + * Retrieves a paginated set of indexable instances of public indexables.
237 + *
238 + * @param int $page The page number (1-based).
239 + * @param int $page_size The number of items per page.
240 + * @param string $post_type The post type indexables to find.
241 + *
242 + * @return Indexable[] The array with the paginated indexable instances which are public.
243 + */
244 + public function find_all_public_paginated( int $page, int $page_size, string $post_type ): array {
245 + $offset = ( ( $page - 1 ) * $page_size );
246 +
247 + $query = $this->query()->where_raw( '( is_public IS NULL OR is_public = 1 ) AND ( is_robots_noindex IS NULL OR is_robots_noindex = 0 )' );
248 + $query->where( 'object_sub_type', $post_type );
249 + $query->where( 'post_status', 'publish' );
250 +
251 + $indexables = $query->order_by_asc( 'id' )->limit( $page_size )->offset( $offset )->find_many();
252 +
253 + return \array_map( [ $this, 'upgrade_indexable' ], $indexables );
254 + }
255 +
256 + /**
220 257 * Retrieves the homepage indexable.
221 258 *
222 259 * @param bool $auto_create Optional. Create the indexable if it does not exist.
223 260 *
@@ -375,8 +412,18 @@
375 412 }
376 413
377 414 $indexables_to_create = \array_diff( $object_ids, $indexables_available );
378 415
416 + if ( ! empty( $indexables_to_create ) ) {
417 + // Warm the object caches for the whole batch, so each build below does not trigger its own uncached queries.
418 + if ( $object_type === 'post' ) {
419 + \_prime_post_caches( $indexables_to_create );
420 + }
421 + elseif ( $object_type === 'term' ) {
422 + \_prime_term_caches( $indexables_to_create );
423 + }
424 + }
425 +
379 426 foreach ( $indexables_to_create as $indexable_to_create ) {
380 427 $indexables[] = $this->builder->build_for_id_and_type( $indexable_to_create, $object_type );
381 428 }
382 429 }
@@ -386,9 +433,9 @@
386 433
387 434 /**
388 435 * Finds the indexables by id's.
389 436 *
390 - * @param array $indexable_ids The indexable id's.
437 + * @param int[] $indexable_ids The indexable id's.
391 438 *
392 439 * @return Indexable[] The found indexables.
393 440 */
394 441 public function find_by_ids( array $indexable_ids ) {
@@ -443,9 +490,9 @@
443 490 /**
444 491 * Returns all subpages with a given post_parent.
445 492 *
446 493 * @param int $post_parent The post parent.
447 - * @param array $exclude_ids The id's to exclude.
494 + * @param int[] $exclude_ids The id's to exclude.
448 495 *
449 496 * @return Indexable[] array of indexables.
450 497 */
451 498 public function get_subpages_by_post_parent( $post_parent, $exclude_ids = [] ) {
@@ -460,8 +507,236 @@
460 507 return $query->find_many();
461 508 }
462 509
463 510 /**
511 + * Returns most recently modified posts of a post type.
512 + *
513 + * @param string $post_type The post type.
514 + * @param int $limit The maximum number of posts to return.
515 + * @param bool $exclude_older_than_one_year Whether to exclude posts older than one year.
516 + * @param string $search_filter Optional. A search filter to apply to the breadcrumb title.
517 + *
518 + * @return Indexable[] array of indexables.
519 + */
520 + public function get_recently_modified_posts( string $post_type, int $limit, bool $exclude_older_than_one_year, string $search_filter = '' ) {
521 + $query = $this->query()
522 + ->where( 'object_type', 'post' )
523 + ->where( 'object_sub_type', $post_type )
524 + ->where_raw( '( is_public IS NULL OR is_public = 1 )' )
525 + ->order_by_desc( 'object_last_modified' )
526 + ->limit( $limit );
527 +
528 + if ( $exclude_older_than_one_year === true ) {
529 + $query->where_gte( 'object_published_at', \gmdate( 'Y-m-d H:i:s', \strtotime( '-1 year' ) ) );
530 + }
531 +
532 + if ( $search_filter !== '' ) {
533 + $query->where_like( 'breadcrumb_title', '%' . $search_filter . '%' );
534 + }
535 +
536 + $query->order_by_desc( 'object_last_modified' )
537 + ->limit( $limit );
538 +
539 + return $query->find_many();
540 + }
541 +
542 + /**
543 + * Finds posts whose breadcrumb title contains any of the given comma-separated phrases.
544 + *
545 + * The search string is a comma-separated list. Each value is matched as a whole
546 + * contiguous substring of the breadcrumb title, and a post is returned when it
547 + * contains any one of the values (a logical OR between values). For example,
548 + * "hiking boots, trail" returns posts whose title contains "hiking boots" or "trail".
549 + *
550 + * Results are paginated and ordered most recently modified first (with the indexable
551 + * id as a stable tiebreaker), so requesting a later page returns older matches.
552 + *
553 + * At most self::MAX_TITLE_KEYWORD_PHRASES phrases are honoured; any beyond that are ignored.
554 + * The page size is clamped to the range 1..self::MAX_TITLE_KEYWORD_PAGE_SIZE.
555 + *
556 + * @param string $keywords The comma-separated phrases to match against the breadcrumb title.
557 + * @param int $page The page of results to return, 1-based.
558 + * @param int $page_size The number of posts per page.
559 + * @param string $post_type The post type to restrict the search to.
560 + *
561 + * @return Indexable[] The matching indexables for the requested page, ordered by most recently modified.
562 + */
563 + public function find_posts_by_title_keywords( string $keywords, int $page = 1, int $page_size = 10, string $post_type = 'post' ) {
564 + $phrases = \array_map( 'trim', \explode( ',', $keywords ) );
565 + $phrases = \array_filter(
566 + $phrases,
567 + static function ( $phrase ) {
568 + return $phrase !== '';
569 + },
570 + );
571 + $phrases = \array_slice( $phrases, 0, self::MAX_TITLE_KEYWORD_PHRASES );
572 +
573 + // An empty search must not degrade into matching every post.
574 + if ( empty( $phrases ) ) {
575 + return [];
576 + }
577 +
578 + $likes = \array_fill( 0, \count( $phrases ), 'breadcrumb_title LIKE %s' );
579 + $params = \array_map(
580 + function ( $phrase ) {
581 + return '%' . $this->wpdb->esc_like( $phrase ) . '%';
582 + },
583 + $phrases,
584 + );
585 +
586 + $page_size = \min( \max( 1, $page_size ), self::MAX_TITLE_KEYWORD_PAGE_SIZE );
587 + $offset = ( ( \max( 1, $page ) - 1 ) * $page_size );
588 +
589 + $indexables = $this->query()
590 + ->where( 'object_type', 'post' )
591 + ->where( 'object_sub_type', $post_type )
592 + ->where_raw( '( ' . \implode( ' OR ', $likes ) . ' )', \array_values( $params ) )
593 + ->order_by_desc( 'object_last_modified' )
594 + ->order_by_desc( 'id' )
595 + ->limit( $page_size )
596 + ->offset( $offset )
597 + ->find_many();
598 +
599 + return \array_map( [ $this, 'upgrade_indexable' ], $indexables );
600 + }
601 +
602 + /**
603 + * Returns the most recently modified cornerstone content of a post type.
604 + *
605 + * @param string $post_type The post type.
606 + * @param int|null $limit The maximum number of posts to return.
607 + *
608 + * @return Indexable[] array of indexables.
609 + */
610 + public function get_recent_cornerstone_for_post_type( string $post_type, ?int $limit ) {
611 + $query = $this->query()
612 + ->where( 'object_type', 'post' )
613 + ->where( 'object_sub_type', $post_type )
614 + ->where_raw( '( is_public IS NULL OR is_public = 1 )' )
615 + ->where( 'is_cornerstone', 1 )
616 + ->order_by_desc( 'object_last_modified' );
617 +
618 + if ( $limit !== null ) {
619 + $query->limit( $limit );
620 + }
621 +
622 + return $query->find_many();
623 + }
624 +
625 + /**
626 + * Returns the most recently modified about page based on schema_page_type.
627 + *
628 + * @param string $post_type The post type.
629 + *
630 + * @return Indexable|false The about page if its there.
631 + */
632 + public function get_most_recent_about_page( $post_type ) {
633 + $query = $this->query()
634 + ->where( 'object_type', 'post' )
635 + ->where( 'object_sub_type', $post_type )
636 + ->where( 'schema_page_type', 'AboutPage' )
637 + ->where_raw( '( is_public IS NULL OR is_public = 1 )' )
638 + ->order_by_desc( 'object_last_modified' );
639 +
640 + return $query->find_one();
641 + }
642 +
643 + /**
644 + * Returns the most recently modified posts with keywords of a post type.
645 + *
646 + * @param string $post_type The post type.
647 + * @param int|null $limit The maximum number of posts to return.
648 + * @param string|null $date_limit Only include content modified after this date.
649 + *
650 + * @return array<array<string, string>>|false The array of indexable columns. False if the query failed.
651 + */
652 + public function get_recent_posts_with_keywords_for_post_type( string $post_type, ?int $limit = null, ?string $date_limit = null ) {
653 + $query = $this->query()
654 + ->select( 'object_id' )
655 + ->select( 'primary_focus_keyword_score' )
656 + ->select( 'breadcrumb_title' )
657 + ->where( 'object_type', 'post' )
658 + ->where( 'object_sub_type', $post_type )
659 + ->where_not_equal( 'primary_focus_keyword_score', 0 )
660 + ->where_not_null( 'primary_focus_keyword_score' )
661 + ->where_raw( "( post_status = 'publish' OR post_status IS NULL )" )
662 + ->where_raw( '( is_robots_noindex IS NULL OR is_robots_noindex <> 1 )' )
663 + ->order_by_desc( 'object_last_modified' );
664 +
665 + if ( $limit !== null ) {
666 + $query->limit( $limit );
667 + }
668 +
669 + if ( $date_limit !== null ) {
670 + $query->where_gte( 'object_last_modified', $date_limit );
671 + }
672 +
673 + return $query->find_array();
674 + }
675 +
676 + /**
677 + * Returns the most recently modified posts with readability scores of a post type.
678 + *
679 + * @param string $post_type The post type.
680 + * @param int|null $limit The maximum number of posts to return.
681 + * @param string|null $date_limit Only include content modified after this date.
682 + *
683 + * @return array<array<string, string|null>>|false The array of indexable columns. False if the query failed.
684 + */
685 + public function get_recent_posts_with_readability_scores_for_post_type( string $post_type, ?int $limit = null, ?string $date_limit = null ) {
686 + $query = $this->query()
687 + ->select( 'object_id' )
688 + ->select( 'readability_score' )
689 + ->select( 'breadcrumb_title' )
690 + ->where( 'object_type', 'post' )
691 + ->where( 'object_sub_type', $post_type )
692 + ->where_not_null( 'estimated_reading_time_minutes' )
693 + ->where_raw( "( post_status = 'publish' OR post_status IS NULL )" )
694 + ->order_by_desc( 'object_last_modified' );
695 +
696 + if ( $limit !== null ) {
697 + $query->limit( $limit );
698 + }
699 +
700 + if ( $date_limit !== null ) {
701 + $query->where_gte( 'object_last_modified', $date_limit );
702 + }
703 +
704 + return $query->find_array();
705 + }
706 +
707 + /**
708 + * Returns the most recently modified posts for a post type.
709 + *
710 + * @param string $post_type The post type.
711 + * @param int|null $limit The maximum number of posts to return.
712 + * @param string|null $date_limit Only include content modified after this date.
713 + *
714 + * @return array<array<string, string|null>>|false The array of indexable columns. False if the query failed.
715 + */
716 + public function get_recent_posts_for_post_type( string $post_type, ?int $limit = null, ?string $date_limit = null ) {
717 + $query = $this->query()
718 + ->select( 'object_id' )
719 + ->select( 'breadcrumb_title' )
720 + ->select( 'description' )
721 + ->where( 'object_type', 'post' )
722 + ->where( 'object_sub_type', $post_type )
723 + ->where_raw( "( post_status = 'publish' OR post_status IS NULL )" )
724 + ->where_raw( '( is_robots_noindex IS NULL OR is_robots_noindex <> 1 )' )
725 + ->order_by_desc( 'object_last_modified' );
726 +
727 + if ( $limit !== null ) {
728 + $query->limit( $limit );
729 + }
730 +
731 + if ( $date_limit !== null ) {
732 + $query->where_gte( 'object_last_modified', $date_limit );
733 + }
734 +
735 + return $query->find_array();
736 + }
737 +
738 + /**
464 739 * Updates the incoming link count for an indexable without first fetching it.
465 740 *
466 741 * @param int $indexable_id The indexable id.
467 742 * @param int $count The incoming link count.
@@ -488,9 +763,9 @@
488 763 */
489 764 public function ensure_permalink( $indexable ) {
490 765 // @phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- self::class is safe.
491 766 // @phpcs:ignore Squiz.PHP.CommentedOutCode.Found
492 - // _deprecated_function( __METHOD__, 'WPSEO 17.3', self::class . '::upgrade_indexable' );
767 + // _deprecated_function( __METHOD__, 'Yoast SEO 17.3', self::class . '::upgrade_indexable' );
493 768
494 769 return $this->upgrade_indexable( $indexable );
495 770 }
496 771
@@ -510,20 +785,21 @@
510 785
511 786 /**
512 787 * Resets the permalinks of the passed object type and subtype.
513 788 *
514 - * @param string|null $type The type of the indexable. Can be null.
515 - * @param string|null $subtype The subtype. Can be null.
789 + * @param string|null $type The type of the indexable. Can be null.
790 + * @param string|null $subtype The subtype. Can be null.
791 + * @param int|null $object_id The object ID. Can be null.
516 792 *
517 793 * @return int|bool The number of permalinks changed if the query was succesful. False otherwise.
518 794 */
519 - public function reset_permalink( $type = null, $subtype = null ) {
795 + public function reset_permalink( $type = null, $subtype = null, $object_id = null ) {
520 796 $query = $this->query()->set(
521 797 [
522 798 'permalink' => null,
523 799 'permalink_hash' => null,
524 800 'version' => 0,
525 - ]
801 + ],
526 802 );
527 803
528 804 if ( $type !== null ) {
529 805 $query->where( 'object_type', $type );
@@ -532,7 +808,20 @@
532 808 if ( $type !== null && $subtype !== null ) {
533 809 $query->where( 'object_sub_type', $subtype );
534 810 }
535 811
812 + if ( $object_id !== null ) {
813 + $query->where( 'object_id', $object_id );
814 + }
815 +
536 816 return $query->update_many();
817 + }
818 +
819 + /**
820 + * Gets the total number of stored indexables.
821 + *
822 + * @return int The total number of stored indexables.
823 + */
824 + public function get_total_number_of_indexables() {
825 + return $this->query()->count();
537 826 }
538 827 }