-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathQueryFilter.php
More file actions
170 lines (146 loc) · 5.94 KB
/
Copy pathQueryFilter.php
File metadata and controls
170 lines (146 loc) · 5.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
<?php
declare(strict_types=1);
namespace NextPostsBlock;
use WP_Block;
use WP_REST_Request;
/**
* Server-side hooks that rewrite a core/query block's SQL args when it
* carries our variation namespace.
*
* Uses the native Query Loop orderBy/order settings to build the canonical
* list. The resolver always traverses forward — the list sort order itself
* determines what "next" means (chronological, reverse-chrono, A-Z, Z-A).
*
* Architecture: pre_render_block sets a static flag for the parent
* core/query block. query_loop_block_query_vars reads and consumes it.
* See previous commit messages for the full rationale.
*
* Non-singular fallback (v1.1.0): when no current post can be detected
* (archive / home / search), the block renders the first N items of the
* canonical list in the chosen sort order instead of returning empty.
*
* Sticky neutralization (v1.2.0): always forces `ignore_sticky_posts = 1`
* and clears `post__not_in` (except when our own excludeSticky flag
* expands the canonical list filter). This prevents WP_Query from
* prepending sticky posts to our result set (which would inflate the
* count beyond perPage) and prevents the native "Sticky posts" control
* from silently filtering our resolved IDs.
*/
final class QueryFilter
{
private const NAMESPACE = 'next-posts-block/query';
private const MIN_COUNT = 1;
private const MAX_COUNT = 10;
/** @var bool Whether the currently-rendering core/query is our variation. */
private static bool $is_sequential = false;
/** @var array<string, mixed> Captured native query attrs from pre_render. */
private static array $query_attrs = [];
/**
* Hook: pre_render_block (priority 10, 2 args)
*
* Fires for the parent core/query block BEFORE its children render.
*
* @param mixed $pre_render
* @param array<string, mixed> $parsed_block
* @return mixed
*/
public function pre_render($pre_render, array $parsed_block)
{
if (($parsed_block['blockName'] ?? '') !== 'core/query') {
return $pre_render;
}
// Reset after the blockName check: we still want a non-matching core/query
// (unrelated sibling) to clear any state leaked from a previous armed-but-
// not-consumed core/query, but inner blocks (core/post-template, etc.) must
// not wipe the state we just armed, as they fire pre_render_block between
// our core/query's pre_render and query_loop_block_query_vars consumption.
self::$is_sequential = false;
self::$query_attrs = [];
if (($parsed_block['attrs']['namespace'] ?? '') !== self::NAMESPACE) {
return $pre_render;
}
self::$is_sequential = true;
self::$query_attrs = (array) ($parsed_block['attrs']['query'] ?? []);
return $pre_render;
}
/**
* Hook: query_loop_block_query_vars (priority 10, 3 args)
*
* @param array<string, mixed> $query
* @return array<string, mixed>
*/
public function filter_query_vars(array $query, WP_Block $block, int $page): array
{
if (!self::$is_sequential) {
return $query;
}
self::$is_sequential = false;
$attrs = self::$query_attrs;
self::$query_attrs = [];
$raw_count = (int) ($block->context['query']['perPage'] ?? 3);
$count = max(self::MIN_COUNT, min(self::MAX_COUNT, $raw_count));
// Build canonical list from the full native attrs bag; resolver
// always walks forward, so the list order defines "next".
$all_ids = CanonicalList::build($attrs);
$current_id = (new ContextDetector())->current_post_id();
$resolved = $current_id === null
? array_slice($all_ids, 0, $count)
: (new SequentialResolver())->resolve($all_ids, $current_id, 'asc', $count);
if (empty($resolved)) {
return array_merge($query, [
'post__in' => [0],
'post__not_in' => [],
'posts_per_page' => 1,
'ignore_sticky_posts' => 1,
]);
}
return array_merge($query, [
'post__in' => $resolved,
'post__not_in' => [],
'orderby' => 'post__in',
'order' => 'ASC',
'posts_per_page' => count($resolved),
'ignore_sticky_posts' => 1,
]);
}
/**
* Hook: rest_{$post_type}_query
*
* @param array<string, mixed> $args
* @return array<string, mixed>
*/
public function filter_rest_query(string $post_type, array $args, WP_REST_Request $request): array
{
$is_sequential_block = (bool) $request->get_param('sequential_block');
$context_post = (int) $request->get_param('sequential_context_post');
// Marker absent AND no context post → not our request.
if (!$is_sequential_block && !$context_post) {
return $args;
}
$raw_attrs = $request->get_param('sequential_query_attrs');
$attrs = [];
if (is_string($raw_attrs) && $raw_attrs !== '') {
$decoded = json_decode($raw_attrs, true);
if (is_array($decoded)) {
$attrs = $decoded;
}
}
// REST route is post-type scoped; respect that if attrs omit postType.
if (empty($attrs['postType'])) {
$attrs['postType'] = $post_type;
}
$raw_count = (int) ($args['posts_per_page'] ?? 3);
$count = max(self::MIN_COUNT, min(self::MAX_COUNT, $raw_count));
$all_ids = CanonicalList::build($attrs);
$resolved = $context_post
? (new SequentialResolver())->resolve($all_ids, $context_post, 'asc', $count)
: array_slice($all_ids, 0, $count);
return array_merge($args, [
'post__in' => $resolved ?: [0],
'post__not_in' => [],
'orderby' => 'post__in',
'order' => 'ASC',
'ignore_sticky_posts' => 1,
]);
}
}