1<?php
2/**
3 * Meta API: WP_Metadata_Lazyloader class
4 *
5 * @package WordPress
6 * @subpackage Meta
7 * @since 4.5.0
8 */
9
10/**
11 * Core class used for lazy-loading object metadata.
12 *
13 * When loading many objects of a given type, such as posts in a WP_Query loop, it often makes
14 * sense to prime various metadata caches at the beginning of the loop. This means fetching all
15 * relevant metadata with a single database query, a technique that has the potential to improve
16 * performance dramatically in some cases.
17 *
18 * In cases where the given metadata may not even be used in the loop, we can improve performance
19 * even more by only priming the metadata cache for affected items the first time a piece of metadata
20 * is requested - ie, by lazy-loading it. So, for example, comment meta may not be loaded into the
21 * cache in the comments section of a post until the first time get_comment_meta() is called in the
22 * context of the comment loop.
23 *
24 * WP uses the WP_Metadata_Lazyloader class to queue objects for metadata cache priming. The class
25 * then detects the relevant get_*_meta() function call, and queries the metadata of all queued objects.
26 *
27 * Do not access this class directly. Use the wp_metadata_lazyloader() function.
28 *
29 * @since 4.5.0
30 */
31#[AllowDynamicProperties]
32class WP_Metadata_Lazyloader {
33 /**
34 * Pending objects queue.
35 *
36 * @since 4.5.0
37 * @var array
38 */
39 protected $pending_objects;
40
41 /**
42 * Settings for supported object types.
43 *
44 * @since 4.5.0
45 * @var array
46 */
47 protected $settings = array();
48
49 /**
50 * Constructor.
51 *
52 * @since 4.5.0
53 */
54 public function __construct() {
55 $this->settings = array(
56 'term' => array(
57 'filter' => 'get_term_metadata',
58 'callback' => array( $this, 'lazyload_meta_callback' ),
59 ),
60 'comment' => array(
61 'filter' => 'get_comment_metadata',
62 'callback' => array( $this, 'lazyload_meta_callback' ),
63 ),
64 'blog' => array(
65 'filter' => 'get_blog_metadata',
66 'callback' => array( $this, 'lazyload_meta_callback' ),
67 ),
68 );
69 }
70
71 /**
72 * Adds objects to the metadata lazy-load queue.
73 *
74 * @since 4.5.0
75 *
76 * @param string $object_type Type of object whose meta is to be lazy-loaded. Accepts 'term' or 'comment'.
77 * @param array $object_ids Array of object IDs.
78 * @return void|WP_Error WP_Error on failure.
79 */
80 public function queue_objects( $object_type, $object_ids ) {
81 if ( ! isset( $this->settings[ $object_type ] ) ) {
82 return new WP_Error( 'invalid_object_type', __( 'Invalid object type.' ) );
83 }
84
85 $type_settings = $this->settings[ $object_type ];
86
87 if ( ! isset( $this->pending_objects[ $object_type ] ) ) {
88 $this->pending_objects[ $object_type ] = array();
89 }
90
91 foreach ( $object_ids as $object_id ) {
92 // Keyed by ID for faster lookup.
93 if ( ! isset( $this->pending_objects[ $object_type ][ $object_id ] ) ) {
94 $this->pending_objects[ $object_type ][ $object_id ] = 1;
95 }
96 }
97
98 add_filter( $type_settings['filter'], $type_settings['callback'], 10, 5 );
99
100 /**
101 * Fires after objects are added to the metadata lazy-load queue.
102 *
103 * @since 4.5.0
104 *
105 * @param array $object_ids Array of object IDs.
106 * @param string $object_type Type of object being queued.
107 * @param WP_Metadata_Lazyloader $lazyloader The lazy-loader object.
108 */
109 do_action( 'metadata_lazyloader_queued_objects', $object_ids, $object_type, $this );
110 }
111
112 /**
113 * Resets lazy-load queue for a given object type.
114 *
115 * @since 4.5.0
116 *
117 * @param string $object_type Object type. Accepts 'comment' or 'term'.
118 * @return void|WP_Error WP_Error on failure.
119 */
120 public function reset_queue( $object_type ) {
121 if ( ! isset( $this->settings[ $object_type ] ) ) {
122 return new WP_Error( 'invalid_object_type', __( 'Invalid object type.' ) );
123 }
124
125 $type_settings = $this->settings[ $object_type ];
126
127 $this->pending_objects[ $object_type ] = array();
128 remove_filter( $type_settings['filter'], $type_settings['callback'] );
129 }
130
131 /**
132 * Lazy-loads term meta for queued terms.
133 *
134 * This method is public so that it can be used as a filter callback. As a rule, there
135 * is no need to invoke it directly.
136 *
137 * @since 4.5.0
138 * @deprecated 6.3.0 Use WP_Metadata_Lazyloader::lazyload_meta_callback() instead.
139 *
140 * @param mixed $check The `$check` param passed from the 'get_term_metadata' hook.
141 * @return mixed In order not to short-circuit `get_metadata()`. Generally, this is `null`, but it could be
142 * another value if filtered by a plugin.
143 */
144 public function lazyload_term_meta( $check ) {
145 _deprecated_function( __METHOD__, '6.3.0', 'WP_Metadata_Lazyloader::lazyload_meta_callback' );
146 return $this->lazyload_meta_callback( $check, 0, '', false, 'term' );
147 }
148
149 /**
150 * Lazy-loads comment meta for queued comments.
151 *
152 * This method is public so that it can be used as a filter callback. As a rule, there is no need to invoke it
153 * directly, from either inside or outside the `WP_Query` object.
154 *
155 * @since 4.5.0
156 * @deprecated 6.3.0 Use WP_Metadata_Lazyloader::lazyload_meta_callback() instead.
157 *
158 * @param mixed $check The `$check` param passed from the {@see 'get_comment_metadata'} hook.
159 * @return mixed The original value of `$check`, so as not to short-circuit `get_comment_metadata()`.
160 */
161 public function lazyload_comment_meta( $check ) {
162 _deprecated_function( __METHOD__, '6.3.0', 'WP_Metadata_Lazyloader::lazyload_meta_callback' );
163 return $this->lazyload_meta_callback( $check, 0, '', false, 'comment' );
164 }
165
166 /**
167 * Lazy-loads meta for queued objects.
168 *
169 * This method is public so that it can be used as a filter callback. As a rule, there
170 * is no need to invoke it directly.
171 *
172 * @since 6.3.0
173 *
174 * @param mixed $check The `$check` param passed from the 'get_*_metadata' hook.
175 * @param int $object_id ID of the object metadata is for.
176 * @param string $meta_key Unused.
177 * @param bool $single Unused.
178 * @param string $meta_type Type of object metadata is for. Accepts 'post', 'comment', 'term', 'user',
179 * or any other object type with an associated meta table.
180 * @return mixed In order not to short-circuit `get_metadata()`. Generally, this is `null`, but it could be
181 * another value if filtered by a plugin.
182 */
183 public function lazyload_meta_callback( $check, $object_id, $meta_key, $single, $meta_type ) {
184 if ( empty( $this->pending_objects[ $meta_type ] ) ) {
185 return $check;
186 }
187
188 $object_ids = array_keys( $this->pending_objects[ $meta_type ] );
189 if ( $object_id && ! in_array( $object_id, $object_ids, true ) ) {
190 $object_ids[] = $object_id;
191 }
192
193 update_meta_cache( $meta_type, $object_ids );
194
195 // No need to run again for this set of objects.
196 $this->reset_queue( $meta_type );
197
198 return $check;
199 }
200}
201