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
wordpress-seo / src / builders / indexable-term-builder.php

indexable-term-builder.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 28.5, at src/builders/indexable-term-builder.php

292 lines 8.5 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\Builders;
4
5 use Yoast\WP\SEO\Exceptions\Indexable\Invalid_Term_Exception;
6 use Yoast\WP\SEO\Exceptions\Indexable\Term_Not_Built_Exception;
7 use Yoast\WP\SEO\Exceptions\Indexable\Term_Not_Found_Exception;
8 use Yoast\WP\SEO\Helpers\Post_Helper;
9 use Yoast\WP\SEO\Helpers\Taxonomy_Helper;
10 use Yoast\WP\SEO\Models\Indexable;
11 use Yoast\WP\SEO\Values\Indexables\Indexable_Builder_Versions;
12
13 /**
14 * Term Builder for the indexables.
15 *
16 * Formats the term meta to indexable format.
17 */
18 class Indexable_Term_Builder {
19
20 use Indexable_Social_Image_Trait;
21
22 /**
23 * Holds the taxonomy helper instance.
24 *
25 * @var Taxonomy_Helper
26 */
27 protected $taxonomy_helper;
28
29 /**
30 * The latest version of the Indexable_Term_Builder.
31 *
32 * @var int
33 */
34 protected $version;
35
36 /**
37 * Holds the taxonomy helper instance.
38 *
39 * @var Post_Helper
40 */
41 protected $post_helper;
42
43 /**
44 * Indexable_Term_Builder constructor.
45 *
46 * @param Taxonomy_Helper $taxonomy_helper The taxonomy helper.
47 * @param Indexable_Builder_Versions $versions The latest version of each Indexable Builder.
48 * @param Post_Helper $post_helper The post helper.
49 */
50 public function __construct(
51 Taxonomy_Helper $taxonomy_helper,
52 Indexable_Builder_Versions $versions,
53 Post_Helper $post_helper
54 ) {
55 $this->taxonomy_helper = $taxonomy_helper;
56 $this->version = $versions->get_latest_version_for_type( 'term' );
57 $this->post_helper = $post_helper;
58 }
59
60 /**
61 * Formats the data.
62 *
63 * @param int $term_id ID of the term to save data for.
64 * @param Indexable $indexable The indexable to format.
65 *
66 * @return bool|Indexable The extended indexable. False when unable to build.
67 *
68 * @throws Invalid_Term_Exception When the term is invalid.
69 * @throws Term_Not_Built_Exception When the term is not viewable.
70 * @throws Term_Not_Found_Exception When the term is not found.
71 */
72 public function build( $term_id, $indexable ) {
73 $term = \get_term( $term_id );
74
75 if ( $term === null ) {
76 throw new Term_Not_Found_Exception();
77 }
78
79 if ( \is_wp_error( $term ) ) {
80 throw new Invalid_Term_Exception( $term->get_error_message() );
81 }
82
83 $indexable_taxonomies = $this->taxonomy_helper->get_indexable_taxonomies();
84 if ( ! \in_array( $term->taxonomy, $indexable_taxonomies, true ) ) {
85 throw Term_Not_Built_Exception::because_not_indexable( $term_id );
86 }
87
88 $term_link = \get_term_link( $term, $term->taxonomy );
89
90 if ( \is_wp_error( $term_link ) ) {
91 throw new Invalid_Term_Exception( $term_link->get_error_message() );
92 }
93
94 $term_meta = $this->taxonomy_helper->get_term_meta( $term );
95
96 $indexable->object_id = $term_id;
97 $indexable->object_type = 'term';
98 $indexable->object_sub_type = $term->taxonomy;
99 $indexable->permalink = $term_link;
100 $indexable->blog_id = \get_current_blog_id();
101
102 $indexable->primary_focus_keyword_score = $this->get_keyword_score(
103 $this->get_meta_value( 'wpseo_focuskw', $term_meta ),
104 $this->get_meta_value( 'wpseo_linkdex', $term_meta ),
105 );
106
107 $indexable->is_robots_noindex = $this->get_noindex_value( $this->get_meta_value( 'wpseo_noindex', $term_meta ) );
108 $indexable->is_public = ( $indexable->is_robots_noindex === null ) ? null : ! $indexable->is_robots_noindex;
109
110 $this->reset_social_images( $indexable );
111
112 foreach ( $this->get_indexable_lookup() as $meta_key => $indexable_key ) {
113 $indexable->{$indexable_key} = $this->get_meta_value( $meta_key, $term_meta );
114 }
115
116 if ( empty( $indexable->breadcrumb_title ) ) {
117 $indexable->breadcrumb_title = $term->name;
118 }
119
120 $this->handle_social_images( $indexable );
121
122 $indexable->is_cornerstone = $this->get_meta_value( 'wpseo_is_cornerstone', $term_meta );
123
124 // Not implemented yet.
125 $indexable->is_robots_nofollow = null;
126 $indexable->is_robots_noarchive = null;
127 $indexable->is_robots_noimageindex = null;
128 $indexable->is_robots_nosnippet = null;
129
130 $timestamps = $this->get_object_timestamps( $term_id, $term->taxonomy );
131 $indexable->object_published_at = $timestamps->published_at;
132 $indexable->object_last_modified = $timestamps->last_modified;
133
134 $indexable->version = $this->version;
135
136 return $indexable;
137 }
138
139 /**
140 * Converts the meta noindex value to the indexable value.
141 *
142 * @param string $meta_value Term meta to base the value on.
143 *
144 * @return bool|null
145 */
146 protected function get_noindex_value( $meta_value ) {
147 if ( $meta_value === 'noindex' ) {
148 return true;
149 }
150
151 if ( $meta_value === 'index' ) {
152 return false;
153 }
154
155 return null;
156 }
157
158 /**
159 * Determines the focus keyword score.
160 *
161 * @param string $keyword The focus keyword that is set.
162 * @param int $score The score saved on the meta data.
163 *
164 * @return int|null Score to use.
165 */
166 protected function get_keyword_score( $keyword, $score ) {
167 if ( empty( $keyword ) ) {
168 return null;
169 }
170
171 return $score;
172 }
173
174 /**
175 * Retrieves the lookup table.
176 *
177 * @return array Lookup table for the indexable fields.
178 */
179 protected function get_indexable_lookup() {
180 return [
181 'wpseo_canonical' => 'canonical',
182 'wpseo_focuskw' => 'primary_focus_keyword',
183 'wpseo_title' => 'title',
184 'wpseo_desc' => 'description',
185 'wpseo_content_score' => 'readability_score',
186 'wpseo_inclusive_language_score' => 'inclusive_language_score',
187 'wpseo_bctitle' => 'breadcrumb_title',
188 'wpseo_opengraph-title' => 'open_graph_title',
189 'wpseo_opengraph-description' => 'open_graph_description',
190 'wpseo_opengraph-image' => 'open_graph_image',
191 'wpseo_opengraph-image-id' => 'open_graph_image_id',
192 'wpseo_twitter-title' => 'twitter_title',
193 'wpseo_twitter-description' => 'twitter_description',
194 'wpseo_twitter-image' => 'twitter_image',
195 'wpseo_twitter-image-id' => 'twitter_image_id',
196 ];
197 }
198
199 /**
200 * Retrieves a meta value from the given meta data.
201 *
202 * @param string $meta_key The key to extract.
203 * @param array $term_meta The meta data.
204 *
205 * @return string|null The meta value.
206 */
207 protected function get_meta_value( $meta_key, $term_meta ) {
208 if ( ! $term_meta || ! \array_key_exists( $meta_key, $term_meta ) ) {
209 return null;
210 }
211
212 $value = $term_meta[ $meta_key ];
213 if ( \is_string( $value ) && $value === '' ) {
214 return null;
215 }
216
217 return $value;
218 }
219
220 /**
221 * Finds an alternative image for the social image.
222 *
223 * @param Indexable $indexable The indexable.
224 *
225 * @return array|bool False when not found, array with data when found.
226 */
227 protected function find_alternative_image( Indexable $indexable ) {
228 $content_image = $this->image->get_term_content_image( $indexable->object_id );
229 if ( $content_image ) {
230 return [
231 'image' => $content_image,
232 'source' => 'first-content-image',
233 ];
234 }
235
236 return false;
237 }
238
239 /**
240 * Returns the timestamps for a given term.
241 *
242 * @param int $term_id The term ID.
243 * @param string $taxonomy The taxonomy.
244 *
245 * @return object An object with last_modified and published_at timestamps.
246 */
247 protected function get_object_timestamps( $term_id, $taxonomy ) {
248 global $wpdb;
249 $post_statuses = $this->post_helper->get_public_post_statuses();
250
251 $replacements = [];
252 $replacements[] = 'post_modified_gmt';
253 $replacements[] = 'post_date_gmt';
254 $replacements[] = $wpdb->posts;
255 $replacements[] = $wpdb->term_relationships;
256 $replacements[] = 'object_id';
257 $replacements[] = 'ID';
258 $replacements[] = $wpdb->term_taxonomy;
259 $replacements[] = 'term_taxonomy_id';
260 $replacements[] = 'term_taxonomy_id';
261 $replacements[] = 'taxonomy';
262 $replacements[] = $taxonomy;
263 $replacements[] = 'term_id';
264 $replacements[] = $term_id;
265 $replacements[] = 'post_status';
266 $replacements = \array_merge( $replacements, $post_statuses );
267 $replacements[] = 'post_password';
268
269 //phpcs:disable WordPress.DB.PreparedSQLPlaceholders -- %i placeholder is still not recognized.
270 //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way.
271 //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches.
272 return $wpdb->get_row(
273 $wpdb->prepare(
274 '
275 SELECT MAX(p.%i) AS last_modified, MIN(p.%i) AS published_at
276 FROM %i AS p
277 INNER JOIN %i AS term_rel
278 ON term_rel.%i = p.%i
279 INNER JOIN %i AS term_tax
280 ON term_tax.%i = term_rel.%i
281 AND term_tax.%i = %s
282 AND term_tax.%i = %d
283 WHERE p.%i IN (' . \implode( ', ', \array_fill( 0, \count( $post_statuses ), '%s' ) ) . ")
284 AND p.%i = ''
285 ",
286 $replacements,
287 ),
288 );
289 //phpcs:enable
290 }
291 }
292