PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.4
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.4
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 / integrations / watchers / indexable-ancestor-watcher.php

indexable-ancestor-watcher.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 28.4, at src/integrations/watchers/indexable-ancestor-watcher.php

243 lines 8.2 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\Integrations\Watchers;
4
5 use Yoast\WP\SEO\Builders\Indexable_Hierarchy_Builder;
6 use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
7 use Yoast\WP\SEO\Helpers\Indexable_Helper;
8 use Yoast\WP\SEO\Helpers\Permalink_Helper;
9 use Yoast\WP\SEO\Helpers\Post_Type_Helper;
10 use Yoast\WP\SEO\Integrations\Integration_Interface;
11 use Yoast\WP\SEO\Models\Indexable;
12 use Yoast\WP\SEO\Repositories\Indexable_Hierarchy_Repository;
13 use Yoast\WP\SEO\Repositories\Indexable_Repository;
14
15 /**
16 * Ancestor watcher to update the ancestor's children.
17 *
18 * Updates its children's permalink when the ancestor itself is updated.
19 */
20 class Indexable_Ancestor_Watcher implements Integration_Interface {
21
22 /**
23 * Represents the indexable repository.
24 *
25 * @var Indexable_Repository
26 */
27 protected $indexable_repository;
28
29 /**
30 * Represents the indexable hierarchy builder.
31 *
32 * @var Indexable_Hierarchy_Builder
33 */
34 protected $indexable_hierarchy_builder;
35
36 /**
37 * Represents the indexable hierarchy repository.
38 *
39 * @var Indexable_Hierarchy_Repository
40 */
41 protected $indexable_hierarchy_repository;
42
43 /**
44 * The indexable helper.
45 *
46 * @var Indexable_Helper
47 */
48 private $indexable_helper;
49
50 /**
51 * Represents the permalink helper.
52 *
53 * @var Permalink_Helper
54 */
55 protected $permalink_helper;
56
57 /**
58 * The post type helper.
59 *
60 * @var Post_Type_Helper
61 */
62 protected $post_type_helper;
63
64 /**
65 * Sets the needed dependencies.
66 *
67 * @param Indexable_Repository $indexable_repository The indexable repository.
68 * @param Indexable_Hierarchy_Builder $indexable_hierarchy_builder The indexable hierarchy builder.
69 * @param Indexable_Hierarchy_Repository $indexable_hierarchy_repository The indexable hierarchy repository.
70 * @param Indexable_Helper $indexable_helper The indexable helper.
71 * @param Permalink_Helper $permalink_helper The permalink helper.
72 * @param Post_Type_Helper $post_type_helper The post type helper.
73 */
74 public function __construct(
75 Indexable_Repository $indexable_repository,
76 Indexable_Hierarchy_Builder $indexable_hierarchy_builder,
77 Indexable_Hierarchy_Repository $indexable_hierarchy_repository,
78 Indexable_Helper $indexable_helper,
79 Permalink_Helper $permalink_helper,
80 Post_Type_Helper $post_type_helper
81 ) {
82 $this->indexable_repository = $indexable_repository;
83 $this->indexable_hierarchy_builder = $indexable_hierarchy_builder;
84 $this->indexable_hierarchy_repository = $indexable_hierarchy_repository;
85 $this->indexable_helper = $indexable_helper;
86 $this->permalink_helper = $permalink_helper;
87 $this->post_type_helper = $post_type_helper;
88 }
89
90 /**
91 * Registers the appropriate hooks.
92 *
93 * @return void
94 */
95 public function register_hooks() {
96 \add_action( 'wpseo_save_indexable', [ $this, 'reset_children' ], \PHP_INT_MAX, 2 );
97 }
98
99 /**
100 * Returns the conditionals based on which this loadable should be active.
101 *
102 * @return array<Migrations_Conditional>
103 */
104 public static function get_conditionals() {
105 return [ Migrations_Conditional::class ];
106 }
107
108 /**
109 * If an indexable's permalink has changed, updates its children in the hierarchy table and resets the children's permalink.
110 *
111 * @param Indexable $indexable The indexable.
112 * @param Indexable $indexable_before The old indexable.
113 *
114 * @return bool True if the children were reset.
115 */
116 public function reset_children( $indexable, $indexable_before ) {
117 if ( ! \in_array( $indexable->object_type, [ 'post', 'term' ], true ) ) {
118 return false;
119 }
120
121 // If the permalink was null it means it was reset instead of changed.
122 if ( $indexable->permalink === $indexable_before->permalink || $indexable_before->permalink === null ) {
123 return false;
124 }
125
126 $child_indexable_ids = $this->indexable_hierarchy_repository->find_children( $indexable );
127 $child_indexables = $this->indexable_repository->find_by_ids( $child_indexable_ids );
128
129 \array_walk( $child_indexables, [ $this, 'update_hierarchy_and_permalink' ] );
130 if ( $indexable->object_type === 'term' ) {
131 $child_indexables_for_term = $this->get_children_for_term( $indexable->object_id, $child_indexables );
132
133 \array_walk( $child_indexables_for_term, [ $this, 'update_hierarchy_and_permalink' ] );
134 }
135
136 return true;
137 }
138
139 /**
140 * Finds all child indexables for the given term.
141 *
142 * @param int $term_id Term to fetch the indexable for.
143 * @param array<Indexable> $child_indexables The already known child indexables.
144 *
145 * @return array<Indexable> The list of additional child indexables for a given term.
146 */
147 public function get_children_for_term( $term_id, array $child_indexables ) {
148 // Finds object_ids (posts) for the term.
149 $post_object_ids = $this->get_object_ids_for_term( $term_id, $child_indexables );
150
151 // Removes the objects that are already present in the children.
152 $existing_post_indexables = \array_filter(
153 $child_indexables,
154 static function ( $indexable ) {
155 return $indexable->object_type === 'post';
156 },
157 );
158
159 $existing_post_object_ids = \wp_list_pluck( $existing_post_indexables, 'object_id' );
160 $post_object_ids = \array_diff( $post_object_ids, $existing_post_object_ids );
161
162 // Finds the indexables for the fetched post_object_ids.
163 $post_indexables = $this->indexable_repository->find_by_multiple_ids_and_type( $post_object_ids, 'post', false );
164
165 // Finds the indexables for the posts that are attached to the term.
166 $post_indexable_ids = \wp_list_pluck( $post_indexables, 'id' );
167 $additional_indexable_ids = $this->indexable_hierarchy_repository->find_children_by_ancestor_ids( $post_indexable_ids );
168
169 // Makes sure we only have indexable id's that we haven't fetched before.
170 $additional_indexable_ids = \array_diff( $additional_indexable_ids, $post_indexable_ids );
171
172 // Finds the additional indexables.
173 $additional_indexables = $this->indexable_repository->find_by_ids( $additional_indexable_ids );
174
175 // Merges all fetched indexables.
176 return \array_merge( $post_indexables, $additional_indexables );
177 }
178
179 /**
180 * Updates the indexable hierarchy and indexable permalink.
181 *
182 * @param Indexable $indexable The indexable to update the hierarchy and permalink for.
183 *
184 * @return void
185 */
186 protected function update_hierarchy_and_permalink( $indexable ) {
187 if ( \is_a( $indexable, Indexable::class ) ) {
188 $this->indexable_hierarchy_builder->build( $indexable );
189
190 $indexable->permalink = $this->permalink_helper->get_permalink_for_indexable( $indexable );
191 $this->indexable_helper->save_indexable( $indexable );
192 }
193 }
194
195 /**
196 * Retrieves the object id's for a term based on the term-post relationship.
197 *
198 * @param int $term_id The term to get the object id's for.
199 * @param array<Indexable> $child_indexables The child indexables.
200 *
201 * @return array<int> List with object ids for the term.
202 */
203 protected function get_object_ids_for_term( $term_id, $child_indexables ) {
204 global $wpdb;
205
206 $filter_terms = static function ( $child ) {
207 return $child->object_type === 'term';
208 };
209
210 $child_terms = \array_filter( $child_indexables, $filter_terms );
211 $child_object_ids = \array_merge( [ $term_id ], \wp_list_pluck( $child_terms, 'object_id' ) );
212
213 // Get the term-taxonomy id's for the term and its children.
214 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
215 $term_taxonomy_ids = $wpdb->get_col(
216 $wpdb->prepare(
217 'SELECT term_taxonomy_id
218 FROM %i
219 WHERE term_id IN( ' . \implode( ', ', \array_fill( 0, ( \count( $child_object_ids ) ), '%s' ) ) . ' )',
220 $wpdb->term_taxonomy,
221 ...$child_object_ids,
222 ),
223 );
224
225 // In the case of faulty data having been saved the above query can return 0 results.
226 if ( empty( $term_taxonomy_ids ) ) {
227 return [];
228 }
229
230 // Get the (post) object id's that are attached to the term.
231 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
232 return $wpdb->get_col(
233 $wpdb->prepare(
234 'SELECT DISTINCT object_id
235 FROM %i
236 WHERE term_taxonomy_id IN( ' . \implode( ', ', \array_fill( 0, \count( $term_taxonomy_ids ), '%s' ) ) . ' )',
237 $wpdb->term_relationships,
238 ...$term_taxonomy_ids,
239 ),
240 );
241 }
242 }
243