• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Yoast / wordpress-seo / e2f53222cf97fe912f41d61152981b5b595f5500

01 Jul 2025 09:02AM UTC coverage: 53.694% (+0.9%) from 52.787%
e2f53222cf97fe912f41d61152981b5b595f5500

push

github

YoastBot
Bump version to 25.4 on free

8218 of 14291 branches covered (57.5%)

Branch coverage included in aggregate %.

30333 of 57507 relevant lines covered (52.75%)

41519.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

30.46
/src/repositories/indexable-repository.php
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() {
2✔
102
                return Model::of_type( 'Indexable' );
2✔
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 there is a database error.
111
         */
112
        public function for_current_page() {
×
113
                $indexable = false;
×
114

115
                switch ( true ) {
116
                        case $this->current_page->is_simple_page():
×
117
                                $indexable = $this->find_by_id_and_type( $this->current_page->get_simple_page_id(), 'post' );
×
118
                                break;
×
119
                        case $this->current_page->is_home_static_page():
×
120
                                $indexable = $this->find_by_id_and_type( $this->current_page->get_front_page_id(), 'post' );
×
121
                                break;
×
122
                        case $this->current_page->is_home_posts_page():
×
123
                                $indexable = $this->find_for_home_page();
×
124
                                break;
×
125
                        case $this->current_page->is_term_archive():
×
126
                                $indexable = $this->find_by_id_and_type( $this->current_page->get_term_id(), 'term' );
×
127
                                break;
×
128
                        case $this->current_page->is_date_archive():
×
129
                                $indexable = $this->find_for_date_archive();
×
130
                                break;
×
131
                        case $this->current_page->is_search_result():
×
132
                                $indexable = $this->find_for_system_page( 'search-result' );
×
133
                                break;
×
134
                        case $this->current_page->is_post_type_archive():
×
135
                                $indexable = $this->find_for_post_type_archive( $this->current_page->get_queried_post_type() );
×
136
                                break;
×
137
                        case $this->current_page->is_author_archive():
×
138
                                $indexable = $this->find_by_id_and_type( $this->current_page->get_author_id(), 'user' );
×
139
                                break;
×
140
                        case $this->current_page->is_404():
×
141
                                $indexable = $this->find_for_system_page( '404' );
×
142
                                break;
×
143
                }
144

145
                if ( $indexable === false ) {
×
146
                        return $this->query()->create(
×
147
                                [
×
148
                                        'object_type' => 'unknown',
×
149
                                        'post_status' => 'unindexed',
×
150
                                        'version'     => 1,
×
151
                                ]
×
152
                        );
×
153
                }
154

155
                return $indexable;
×
156
        }
157

158
        /**
159
         * Retrieves an indexable by its permalink.
160
         *
161
         * @param string $permalink The indexable permalink.
162
         *
163
         * @return bool|Indexable The indexable, false if none could be found.
164
         */
165
        public function find_by_permalink( $permalink ) {
×
166
                $permalink_hash = \strlen( $permalink ) . ':' . \md5( $permalink );
×
167

168
                // Find by both permalink_hash and permalink, permalink_hash is indexed so will be used first by the DB to optimize the query.
169
                return $this->query()
×
170
                        ->where( 'permalink_hash', $permalink_hash )
×
171
                        ->where( 'permalink', $permalink )
×
172
                        ->find_one();
×
173
        }
174

175
        /**
176
         * Retrieves all the indexable instances of a certain object type.
177
         *
178
         * @param string $object_type The object type.
179
         *
180
         * @return Indexable[] The array with all the indexable instances of a certain object type.
181
         */
182
        public function find_all_with_type( $object_type ) {
×
183
                /**
184
                 * The array with all the indexable instances of a certain object type.
185
                 *
186
                 * @var Indexable[] $indexables
187
                 */
188
                $indexables = $this
×
189
                        ->query()
×
190
                        ->where( 'object_type', $object_type )
×
191
                        ->find_many();
×
192

193
                return \array_map( [ $this, 'upgrade_indexable' ], $indexables );
×
194
        }
195

196
        /**
197
         * Retrieves all the indexable instances of a certain object subtype.
198
         *
199
         * @param string $object_type     The object type.
200
         * @param string $object_sub_type The object subtype.
201
         *
202
         * @return Indexable[] The array with all the indexable instances of a certain object subtype.
203
         */
204
        public function find_all_with_type_and_sub_type( $object_type, $object_sub_type ) {
×
205
                /**
206
                 * The array with all the indexable instances of a certain object type and subtype.
207
                 *
208
                 * @var Indexable[] $indexables
209
                 */
210
                $indexables = $this
×
211
                        ->query()
×
212
                        ->where( 'object_type', $object_type )
×
213
                        ->where( 'object_sub_type', $object_sub_type )
×
214
                        ->find_many();
×
215

216
                return \array_map( [ $this, 'upgrade_indexable' ], $indexables );
×
217
        }
218

219
        /**
220
         * Retrieves the homepage indexable.
221
         *
222
         * @param bool $auto_create Optional. Create the indexable if it does not exist.
223
         *
224
         * @return bool|Indexable Instance of indexable.
225
         */
226
        public function find_for_home_page( $auto_create = true ) {
×
227
                $indexable = \wp_cache_get( 'home-page', 'yoast-seo-indexables' );
×
228
                if ( ! $indexable ) {
×
229
                        /**
230
                         * Indexable instance.
231
                         *
232
                         * @var Indexable $indexable
233
                         */
234
                        $indexable = $this->query()->where( 'object_type', 'home-page' )->find_one();
×
235

236
                        if ( $auto_create && ! $indexable ) {
×
237
                                $indexable = $this->builder->build_for_home_page();
×
238
                        }
239

240
                        $indexable = $this->upgrade_indexable( $indexable );
×
241

242
                        \wp_cache_set( 'home-page', $indexable, 'yoast-seo-indexables', ( 5 * \MINUTE_IN_SECONDS ) );
×
243
                }
244

245
                return $indexable;
×
246
        }
247

248
        /**
249
         * Retrieves the date archive indexable.
250
         *
251
         * @param bool $auto_create Optional. Create the indexable if it does not exist.
252
         *
253
         * @return bool|Indexable Instance of indexable.
254
         */
255
        public function find_for_date_archive( $auto_create = true ) {
×
256
                /**
257
                 * Indexable instance.
258
                 *
259
                 * @var Indexable $indexable
260
                 */
261
                $indexable = $this->query()->where( 'object_type', 'date-archive' )->find_one();
×
262

263
                if ( $auto_create && ! $indexable ) {
×
264
                        $indexable = $this->builder->build_for_date_archive();
×
265
                }
266

267
                return $this->upgrade_indexable( $indexable );
×
268
        }
269

270
        /**
271
         * Retrieves an indexable for a post type archive.
272
         *
273
         * @param string $post_type   The post type.
274
         * @param bool   $auto_create Optional. Create the indexable if it does not exist.
275
         *
276
         * @return bool|Indexable The indexable, false if none could be found.
277
         */
278
        public function find_for_post_type_archive( $post_type, $auto_create = true ) {
×
279
                /**
280
                 * Indexable instance.
281
                 *
282
                 * @var Indexable $indexable
283
                 */
284
                $indexable = $this->query()
×
285
                        ->where( 'object_type', 'post-type-archive' )
×
286
                        ->where( 'object_sub_type', $post_type )
×
287
                        ->find_one();
×
288

289
                if ( $auto_create && ! $indexable ) {
×
290
                        $indexable = $this->builder->build_for_post_type_archive( $post_type );
×
291
                }
292

293
                return $this->upgrade_indexable( $indexable );
×
294
        }
295

296
        /**
297
         * Retrieves the indexable for a system page.
298
         *
299
         * @param string $object_sub_type The type of system page.
300
         * @param bool   $auto_create     Optional. Create the indexable if it does not exist.
301
         *
302
         * @return bool|Indexable Instance of indexable.
303
         */
304
        public function find_for_system_page( $object_sub_type, $auto_create = true ) {
×
305
                /**
306
                 * Indexable instance.
307
                 *
308
                 * @var Indexable $indexable
309
                 */
310
                $indexable = $this->query()
×
311
                        ->where( 'object_type', 'system-page' )
×
312
                        ->where( 'object_sub_type', $object_sub_type )
×
313
                        ->find_one();
×
314

315
                if ( $auto_create && ! $indexable ) {
×
316
                        $indexable = $this->builder->build_for_system_page( $object_sub_type );
×
317
                }
318

319
                return $this->upgrade_indexable( $indexable );
×
320
        }
321

322
        /**
323
         * Retrieves an indexable by its ID and type.
324
         *
325
         * @param int    $object_id   The indexable object ID.
326
         * @param string $object_type The indexable object type.
327
         * @param bool   $auto_create Optional. Create the indexable if it does not exist.
328
         *
329
         * @return bool|Indexable Instance of indexable.
330
         */
331
        public function find_by_id_and_type( $object_id, $object_type, $auto_create = true ) {
×
332
                $indexable = $this->query()
×
333
                        ->where( 'object_id', $object_id )
×
334
                        ->where( 'object_type', $object_type )
×
335
                        ->find_one();
×
336

337
                if ( $auto_create && ! $indexable ) {
×
338
                        $indexable = $this->builder->build_for_id_and_type( $object_id, $object_type );
×
339
                }
340
                else {
341
                        $indexable = $this->upgrade_indexable( $indexable );
×
342
                }
343

344
                return $indexable;
×
345
        }
346

347
        /**
348
         * Retrieves multiple indexables at once by their id's and type.
349
         *
350
         * @param int[]  $object_ids  The array of indexable object id's.
351
         * @param string $object_type The indexable object type.
352
         * @param bool   $auto_create Optional. Create the indexable if it does not exist.
353
         *
354
         * @return Indexable[] An array of indexables.
355
         */
356
        public function find_by_multiple_ids_and_type( $object_ids, $object_type, $auto_create = true ) {
×
357
                if ( empty( $object_ids ) ) {
×
358
                        return [];
×
359
                }
360

361
                /**
362
                 * Represents an array of indexable objects.
363
                 *
364
                 * @var Indexable[] $indexables
365
                 */
366
                $indexables = $this->query()
×
367
                        ->where_in( 'object_id', $object_ids )
×
368
                        ->where( 'object_type', $object_type )
×
369
                        ->find_many();
×
370

371
                if ( $auto_create ) {
×
372
                        $indexables_available = [];
×
373
                        foreach ( $indexables as $indexable ) {
×
374
                                $indexables_available[] = $indexable->object_id;
×
375
                        }
376

377
                        $indexables_to_create = \array_diff( $object_ids, $indexables_available );
×
378

379
                        foreach ( $indexables_to_create as $indexable_to_create ) {
×
380
                                $indexables[] = $this->builder->build_for_id_and_type( $indexable_to_create, $object_type );
×
381
                        }
382
                }
383

384
                return \array_map( [ $this, 'upgrade_indexable' ], $indexables );
×
385
        }
386

387
        /**
388
         * Finds the indexables by id's.
389
         *
390
         * @param array $indexable_ids The indexable id's.
391
         *
392
         * @return Indexable[] The found indexables.
393
         */
394
        public function find_by_ids( array $indexable_ids ) {
2✔
395
                if ( empty( $indexable_ids ) ) {
2✔
396
                        return [];
×
397
                }
398

399
                $indexables = $this
2✔
400
                        ->query()
2✔
401
                        ->where_in( 'id', $indexable_ids )
2✔
402
                        ->find_many();
2✔
403

404
                return \array_map( [ $this, 'upgrade_indexable' ], $indexables );
2✔
405
        }
406

407
        /**
408
         * Returns all ancestors of a given indexable.
409
         *
410
         * @param Indexable $indexable The indexable to find the ancestors of.
411
         *
412
         * @return Indexable[] All ancestors of the given indexable.
413
         */
414
        public function get_ancestors( Indexable $indexable ) {
14✔
415
                // If we've already set ancestors on the indexable no need to get them again.
416
                if ( \is_array( $indexable->ancestors ) && ! empty( $indexable->ancestors ) ) {
14✔
417
                        return \array_map( [ $this, 'upgrade_indexable' ], $indexable->ancestors );
×
418
                }
419

420
                $indexable_ids = $this->hierarchy_repository->find_ancestors( $indexable );
14✔
421

422
                // If we've set ancestors on the indexable because we had to build them to find them.
423
                if ( \is_array( $indexable->ancestors ) && ! empty( $indexable->ancestors ) ) {
14✔
424
                        return \array_map( [ $this, 'upgrade_indexable' ], $indexable->ancestors );
×
425
                }
426

427
                if ( empty( $indexable_ids ) ) {
14✔
428
                        return [];
2✔
429
                }
430

431
                if ( $indexable_ids[0] === 0 && \count( $indexable_ids ) === 1 ) {
12✔
432
                        return [];
×
433
                }
434

435
                $indexables = $this->query()
12✔
436
                        ->where_in( 'id', $indexable_ids )
12✔
437
                        ->order_by_expr( 'FIELD(id,' . \implode( ',', $indexable_ids ) . ')' )
12✔
438
                        ->find_many();
12✔
439

440
                return \array_map( [ $this, 'upgrade_indexable' ], $indexables );
12✔
441
        }
442

443
        /**
444
         * Returns all subpages with a given post_parent.
445
         *
446
         * @param int   $post_parent The post parent.
447
         * @param array $exclude_ids The id's to exclude.
448
         *
449
         * @return Indexable[] array of indexables.
450
         */
451
        public function get_subpages_by_post_parent( $post_parent, $exclude_ids = [] ) {
×
452
                $query = $this->query()
×
453
                        ->where( 'post_parent', $post_parent )
×
454
                        ->where( 'object_type', 'post' )
×
455
                        ->where( 'post_status', 'publish' );
×
456

457
                if ( ! empty( $exclude_ids ) ) {
×
458
                        $query->where_not_in( 'object_id', $exclude_ids );
×
459
                }
460
                return $query->find_many();
×
461
        }
462

463
        /**
464
         * Returns most recently modified posts of a post type.
465
         *
466
         * @param string $post_type                   The post type.
467
         * @param int    $limit                       The maximum number of posts to return.
468
         * @param bool   $exclude_older_than_one_year Whether to exclude posts older than one year.
469
         *
470
         * @return Indexable[] array of indexables.
471
         */
472
        public function get_recently_modified_posts( string $post_type, int $limit, bool $exclude_older_than_one_year ) {
36✔
473
                $query = $this->query()
36✔
474
                        ->where( 'object_type', 'post' )
36✔
475
                        ->where( 'object_sub_type', $post_type )
36✔
476
                        ->where_raw( '( is_public IS NULL OR is_public = 1 )' )
36✔
477
                        ->order_by_desc( 'object_last_modified' )
36✔
478
                        ->limit( $limit );
36✔
479

480
                if ( $exclude_older_than_one_year === true ) {
36✔
481
                        $query->where_gte( 'object_published_at', \gmdate( 'Y-m-d H:i:s', \strtotime( '-1 year' ) ) );
8✔
482
                }
483

484
                $query->order_by_desc( 'object_last_modified' )
36✔
485
                        ->limit( $limit );
36✔
486

487
                return $query->find_many();
36✔
488
        }
489

490
        /**
491
         * Returns the most recently modified cornerstone content of a post type.
492
         *
493
         * @param string   $post_type The post type.
494
         * @param int|null $limit     The maximum number of posts to return.
495
         *
496
         * @return Indexable[] array of indexables.
497
         */
498
        public function get_recent_cornerstone_for_post_type( string $post_type, ?int $limit ) {
56✔
499
                $query = $this->query()
56✔
500
                        ->where( 'object_type', 'post' )
56✔
501
                        ->where( 'object_sub_type', $post_type )
56✔
502
                        ->where_raw( '( is_public IS NULL OR is_public = 1 )' )
56✔
503
                        ->where( 'is_cornerstone', 1 )
56✔
504
                        ->order_by_desc( 'object_last_modified' );
56✔
505

506
                if ( $limit !== null ) {
56✔
507
                        $query->limit( $limit );
32✔
508
                }
509

510
                return $query->find_many();
56✔
511
        }
512

513
        /**
514
         * Updates the incoming link count for an indexable without first fetching it.
515
         *
516
         * @param int $indexable_id The indexable id.
517
         * @param int $count        The incoming link count.
518
         *
519
         * @return bool Whether or not the update was succeful.
520
         */
521
        public function update_incoming_link_count( $indexable_id, $count ) {
×
522
                return (bool) $this->query()
×
523
                        ->set( 'incoming_link_count', $count )
×
524
                        ->where( 'id', $indexable_id )
×
525
                        ->update_many();
×
526
        }
527

528
        /**
529
         * Ensures that the given indexable has a permalink.
530
         *
531
         * Will be deprecated in 17.3 - Use upgrade_indexable instead.
532
         *
533
         * @codeCoverageIgnore
534
         *
535
         * @param Indexable $indexable The indexable.
536
         *
537
         * @return bool|Indexable The indexable.
538
         */
539
        public function ensure_permalink( $indexable ) {
540
                // @phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- self::class is safe.
541
                // @phpcs:ignore Squiz.PHP.CommentedOutCode.Found
542
                // _deprecated_function( __METHOD__, 'Yoast SEO 17.3', self::class . '::upgrade_indexable' );
543

544
                return $this->upgrade_indexable( $indexable );
545
        }
546

547
        /**
548
         * Checks if an Indexable is outdated, and rebuilds it when necessary.
549
         *
550
         * @param Indexable $indexable The indexable.
551
         *
552
         * @return Indexable The indexable.
553
         */
554
        public function upgrade_indexable( $indexable ) {
6✔
555
                if ( $this->version_manager->indexable_needs_upgrade( $indexable ) ) {
6✔
556
                        $indexable = $this->builder->build( $indexable );
4✔
557
                }
558
                return $indexable;
6✔
559
        }
560

561
        /**
562
         * Resets the permalinks of the passed object type and subtype.
563
         *
564
         * @param string|null $type    The type of the indexable. Can be null.
565
         * @param string|null $subtype The subtype. Can be null.
566
         *
567
         * @return int|bool The number of permalinks changed if the query was succesful. False otherwise.
568
         */
569
        public function reset_permalink( $type = null, $subtype = null ) {
6✔
570
                $query = $this->query()->set(
6✔
571
                        [
6✔
572
                                'permalink'      => null,
6✔
573
                                'permalink_hash' => null,
6✔
574
                                'version'        => 0,
6✔
575
                        ]
6✔
576
                );
6✔
577

578
                if ( $type !== null ) {
6✔
579
                        $query->where( 'object_type', $type );
2✔
580
                }
581

582
                if ( $type !== null && $subtype !== null ) {
6✔
583
                        $query->where( 'object_sub_type', $subtype );
2✔
584
                }
585

586
                return $query->update_many();
6✔
587
        }
588

589
        /**
590
         * Gets the total number of stored indexables.
591
         *
592
         * @return int The total number of stored indexables.
593
         */
594
        public function get_total_number_of_indexables() {
×
595
                return $this->query()->count();
×
596
        }
597
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc