Skip to content

Commit 0a52ffb

Browse files
committed
Posts, Post Types: Add pre_wp_count_attachments filter.
Introduces the `pre_wp_count_attachments` filter to `wp_count_attachments()`, the counterpart of `pre_wp_count_posts`. Returning a `stdClass` object short-circuits the function, skipping the object cache lookup and both of its queries. Since the `counts` cache group is non-persistent, those queries otherwise run on every request that needs the counts, and on a site with millions of attachments the `GROUP BY post_mime_type` aggregate alone can take several seconds. The filter allows the counts to be served instead from a value pre-computed in the background. Unlike post statuses, the set of MIME types is not known ahead of time, so only a missing trash count is filled in with zero. The filtered value should therefore contain a count for every MIME type matching `$mime_type`, and only those types. As with `pre_wp_count_posts`, the value is not cached and is passed through the `wp_count_attachments` filter. The `pre_wp_count_posts` filter introduced in r64245 is brought in line with the new one. A filtered value now short-circuits only when it is a `stdClass` object: any other non-null value was previously cast to an object, turning `false` into a nonsensical result, and filling in missing counts on another class can trigger a dynamic property deprecation. Both filters also clone the returned object, so that a callback which keeps it, such as in a static variable, does not have it modified by core or by later callbacks on the `wp_count_posts` and `wp_count_attachments` filters. Developed in #14210. Follow-up to r64245. Props jdeep, westonruter, peterwilsoncc. See #66098. Fixes #66291. git-svn-id: https://develop.svn.wordpress.org/trunk@64589 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 9fbaa2d commit 0a52ffb

3 files changed

Lines changed: 356 additions & 11 deletions

File tree

‎src/wp-includes/post.php‎

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3544,12 +3544,12 @@ function wp_count_posts( $type = 'post', $perm = '' ) {
35443544
/**
35453545
* Filters the post counts before the query is run.
35463546
*
3547-
* Returning a non-null value short-circuits wp_count_posts(), skipping both
3548-
* the object cache lookup and the database query. This allows the counts to
3549-
* be served from another source, such as a value pre-computed in the
3550-
* background, when an exact real-time count is not required.
3547+
* Returning a stdClass object short-circuits wp_count_posts(), skipping
3548+
* both the object cache lookup and the database query. This allows the
3549+
* counts to be served from another source, such as a value pre-computed in
3550+
* the background, when an exact real-time count is not required.
35513551
*
3552-
* Any registered post status missing from the filtered value are added
3552+
* Any registered post status missing from the filtered value is added
35533553
* to the result and set to zero.
35543554
*
35553555
* The returned value is not cached and passed through the
@@ -3565,8 +3565,8 @@ function wp_count_posts( $type = 'post', $perm = '' ) {
35653565
*/
35663566
$counts = apply_filters( 'pre_wp_count_posts', null, $type, $perm );
35673567

3568-
if ( null !== $counts ) {
3569-
$counts = (object) $counts;
3568+
if ( $counts instanceof stdClass ) {
3569+
$counts = clone $counts;
35703570

35713571
foreach ( get_post_stati() as $status ) {
35723572
if ( ! isset( $counts->{$status} ) ) {
@@ -3669,6 +3669,47 @@ function wp_count_posts( $type = 'post', $perm = '' ) {
36693669
function wp_count_attachments( $mime_type = '' ) {
36703670
global $wpdb;
36713671

3672+
/**
3673+
* Filters the attachment counts before the query is run.
3674+
*
3675+
* Returning a stdClass object short-circuits wp_count_attachments(),
3676+
* skipping both the object cache lookup and the database queries. This
3677+
* allows the counts to be served from another source, such as a value
3678+
* pre-computed in the background, when an exact real-time count is not
3679+
* required.
3680+
*
3681+
* If the filtered value does not contain a trash count, one is added to the
3682+
* result and set to zero. Missing MIME types are not added, as they depend on
3683+
* the attachments that exist. The filtered value should therefore contain a
3684+
* count for every MIME type matching `$mime_type` that has attachments,
3685+
* including types first uploaded after the value was computed, as callers
3686+
* may read the count for any MIME type that exists. It should also contain
3687+
* only MIME types matching `$mime_type`, with a trash count limited to
3688+
* attachments of those types, as the default query does.
3689+
*
3690+
* The returned value is not cached and passed through the
3691+
* {@see 'wp_count_attachments'} filter.
3692+
*
3693+
* @since 7.2.0
3694+
*
3695+
* @param stdClass|null $counts An object containing the attachment counts by
3696+
* mime type, or null to run the default query.
3697+
* Default null.
3698+
* @param string|string[] $mime_type Array or comma-separated list of MIME patterns.
3699+
*/
3700+
$counts = apply_filters( 'pre_wp_count_attachments', null, $mime_type );
3701+
3702+
if ( $counts instanceof stdClass ) {
3703+
$counts = clone $counts;
3704+
3705+
if ( ! isset( $counts->trash ) ) {
3706+
$counts->trash = 0;
3707+
}
3708+
3709+
/** This filter is documented in wp-includes/post.php */
3710+
return apply_filters( 'wp_count_attachments', $counts, $mime_type );
3711+
}
3712+
36723713
$cache_key = sprintf(
36733714
'attachments%s',
36743715
! empty( $mime_type ) ? ':' . str_replace( '/', '_', implode( '-', (array) $mime_type ) ) : ''

‎tests/phpunit/tests/post/wpCountAttachments.php‎

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,225 @@ public function test_wp_count_attachments_should_cache_the_result() {
3131
// Keep assertEquals() because the objects are intentionally compared by value.
3232
$this->assertEquals( $expected, $actual );
3333
}
34+
35+
/**
36+
* @ticket 66291
37+
*/
38+
public function test_pre_wp_count_attachments_short_circuits_query() {
39+
self::factory()->post->create_many(
40+
3,
41+
array(
42+
'post_type' => 'attachment',
43+
'post_mime_type' => 'image/jpeg',
44+
)
45+
);
46+
47+
$mime_type = 'image/jpeg';
48+
$mock_count = 42;
49+
add_filter(
50+
'pre_wp_count_attachments',
51+
static function () use ( $mime_type, $mock_count ) {
52+
return (object) array( $mime_type => $mock_count );
53+
}
54+
);
55+
56+
$start_num_queries = get_num_queries();
57+
$counts = wp_count_attachments();
58+
59+
$this->assertSame( 0, get_num_queries() - $start_num_queries, 'No database query should run when the filter returns a value.' );
60+
$this->assertObjectHasProperty( $mime_type, $counts );
61+
$this->assertSame( $mock_count, $counts->$mime_type, 'Count is expected to be filtered value.' );
62+
$this->assertObjectHasProperty( 'trash', $counts );
63+
$this->assertSame( 0, $counts->trash, 'Expected trash to be set to zero.' );
64+
}
65+
66+
/**
67+
* @ticket 66291
68+
*/
69+
public function test_pre_wp_count_attachments_takes_precedence_over_cached_counts() {
70+
self::factory()->post->create_many(
71+
3,
72+
array(
73+
'post_type' => 'attachment',
74+
'post_mime_type' => 'image/jpeg',
75+
)
76+
);
77+
78+
wp_count_attachments();
79+
$this->assertNotFalse( wp_cache_get( 'attachments', 'counts' ), 'Expected the counts to be cached before the filter is added.' );
80+
81+
$mime_type = 'image/jpeg';
82+
$mock_count = 42;
83+
add_filter(
84+
'pre_wp_count_attachments',
85+
static function () use ( $mime_type, $mock_count ) {
86+
return (object) array( $mime_type => $mock_count );
87+
}
88+
);
89+
90+
$this->assertSame( $mock_count, wp_count_attachments()->$mime_type, 'The filtered value should be returned instead of the cached counts.' );
91+
}
92+
93+
/**
94+
* @ticket 66291
95+
*/
96+
public function test_pre_wp_count_attachments_null_runs_default_query() {
97+
self::factory()->post->create_many(
98+
3,
99+
array(
100+
'post_type' => 'attachment',
101+
'post_mime_type' => 'image/jpeg',
102+
)
103+
);
104+
105+
add_filter( 'pre_wp_count_attachments', '__return_null' );
106+
$start_num_queries = get_num_queries();
107+
108+
$this->assertSame( '3', wp_count_attachments()->{'image/jpeg'}, 'Attachment count is expected to be set.' );
109+
$this->assertGreaterThan( 0, get_num_queries() - $start_num_queries, 'wp_count_attachments() is expected to trigger database queries.' );
110+
}
111+
112+
/**
113+
* @ticket 66291
114+
*
115+
* @dataProvider data_non_stdclass_values
116+
*
117+
* @param mixed $value Filtered value.
118+
*/
119+
public function test_pre_wp_count_attachments_non_stdclass_runs_default_query( $value ) {
120+
self::factory()->post->create_many(
121+
3,
122+
array(
123+
'post_type' => 'attachment',
124+
'post_mime_type' => 'image/jpeg',
125+
)
126+
);
127+
128+
add_filter(
129+
'pre_wp_count_attachments',
130+
static function () use ( $value ) {
131+
return $value;
132+
}
133+
);
134+
$start_num_queries = get_num_queries();
135+
136+
$this->assertSame( '3', wp_count_attachments()->{'image/jpeg'}, 'A filtered value that is not a stdClass object is expected to be ignored.' );
137+
$this->assertGreaterThan( 0, get_num_queries() - $start_num_queries, 'wp_count_attachments() is expected to trigger database queries.' );
138+
}
139+
140+
/**
141+
* Data provider for {@see self::test_pre_wp_count_attachments_non_stdclass_runs_default_query()}.
142+
*
143+
* @return array<non-falsy-string, array{ 0: mixed }>
144+
*/
145+
public function data_non_stdclass_values(): array {
146+
return array(
147+
'array' => array( array( 'image/jpeg' => 42 ) ),
148+
'false' => array( false ),
149+
'ArrayObject' => array( new ArrayObject( array( 'image/jpeg' => 42 ) ) ),
150+
);
151+
}
152+
153+
/**
154+
* @ticket 66291
155+
*/
156+
public function test_pre_wp_count_attachments_receives_mime_type() {
157+
$filter = new MockAction();
158+
add_filter( 'pre_wp_count_attachments', array( $filter, 'filter' ), 10, 2 );
159+
160+
wp_count_attachments( array( 'image/jpeg', 'image/png' ) );
161+
162+
$args = $filter->get_args();
163+
$this->assertSame( array( null, array( 'image/jpeg', 'image/png' ) ), $args[0] );
164+
}
165+
166+
/**
167+
* @ticket 66291
168+
*/
169+
public function test_pre_wp_count_attachments_keeps_filtered_trash_count() {
170+
$trash_count = 7;
171+
add_filter(
172+
'pre_wp_count_attachments',
173+
static function () use ( $trash_count ) {
174+
return (object) array( 'trash' => $trash_count );
175+
}
176+
);
177+
178+
$this->assertSame( $trash_count, wp_count_attachments()->trash, 'Expected trash count to be the filtered value.' );
179+
}
180+
181+
/**
182+
* @ticket 66291
183+
*/
184+
public function test_pre_wp_count_attachments_value_is_passed_to_wp_count_attachments_filter() {
185+
add_filter(
186+
'pre_wp_count_attachments',
187+
static function () {
188+
return (object) array( 'image/jpeg' => 5 );
189+
}
190+
);
191+
$filter = new MockAction();
192+
add_filter( 'wp_count_attachments', array( $filter, 'filter' ), 10, 2 );
193+
194+
wp_count_attachments( 'image' );
195+
196+
$args = $filter->get_args();
197+
$this->assertSame( 1, $filter->get_call_count(), 'Expected the wp_count_attachments filter to run once.' );
198+
$this->assertEquals(
199+
array(
200+
(object) array(
201+
'image/jpeg' => 5,
202+
'trash' => 0,
203+
),
204+
'image',
205+
),
206+
$args[0],
207+
'Expected the wp_count_attachments filter to receive the filtered counts and MIME type.'
208+
);
209+
}
210+
211+
/**
212+
* @ticket 66291
213+
*/
214+
public function test_pre_wp_count_attachments_does_not_modify_filtered_object() {
215+
$stored_counts = (object) array( 'image/jpeg' => 5 );
216+
add_filter(
217+
'pre_wp_count_attachments',
218+
static function () use ( $stored_counts ) {
219+
return $stored_counts;
220+
}
221+
);
222+
223+
$counts = wp_count_attachments();
224+
225+
$this->assertSame( 0, $counts->trash, 'Expected trash to be set to zero.' );
226+
$this->assertEquals( (object) array( 'image/jpeg' => 5 ), $stored_counts, 'The object returned by the filter should not be modified.' );
227+
}
228+
229+
/**
230+
* @ticket 66291
231+
*/
232+
public function test_pre_wp_count_attachments_result_is_not_cached() {
233+
self::factory()->post->create_many(
234+
3,
235+
array(
236+
'post_type' => 'attachment',
237+
'post_mime_type' => 'image/jpeg',
238+
)
239+
);
240+
241+
$filter = static function () {
242+
return (object) array( 'image/jpeg' => 99 );
243+
};
244+
add_filter( 'pre_wp_count_attachments', $filter );
245+
246+
$this->assertSame( 99, wp_count_attachments( 'image/jpeg' )->{'image/jpeg'}, 'Expected the filtered count.' );
247+
$this->assertFalse( wp_cache_get( 'attachments:image_jpeg', 'counts' ), 'Expected the filtered counts not to be cached.' );
248+
249+
remove_filter( 'pre_wp_count_attachments', $filter );
250+
$start_num_queries = get_num_queries();
251+
252+
$this->assertSame( '3', wp_count_attachments( 'image/jpeg' )->{'image/jpeg'}, 'Expected the real count once the filter is removed.' );
253+
$this->assertGreaterThan( 0, get_num_queries() - $start_num_queries, 'Expected database queries since nothing should have been cached.' );
254+
}
34255
}

0 commit comments

Comments
 (0)