PluginProbe
FlyWP Helper – Page Cache, Page Optimization, Emails for FlyWP Server Control Panel / 1.8.0
FlyWP Helper – Page Cache, Page Optimization, Emails for FlyWP Server Control Panel v1.8.0
1.8.0 1.7.1 1.7.0 1.6.0 1.5.2 trunk 0.1 0.2.0 0.2.1 0.3 0.3.1 0.3.2 0.3.3 0.3.4 0.4 0.4.1 0.4.2 0.4.3 1.0 1.1 1.2 1.3.1 1.4.0 1.4.1 1.5.0 All 27 releases
flywp / includes / StaticSite / Engine / Incremental / ChangeSetBuilder.php

ChangeSetBuilder.php in FlyWP Helper – Page Cache, Page Optimization, Emails for FlyWP Server Control Panel 1.8.0, at includes/StaticSite/Engine/Incremental/ChangeSetBuilder.php

403 lines 12.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FlyWP\StaticSite\Engine\Incremental;
4
5 use FlyWP\StaticSite\Engine\Discovery\HomeSource;
6 use FlyWP\StaticSite\Engine\Discovery\OriginUrl;
7 use FlyWP\StaticSite\Engine\Discovery\Pagination;
8 use FlyWP\StaticSite\Engine\Discovery\PostTypeSource;
9 use FlyWP\StaticSite\Engine\Discovery\RobotsMeta;
10 use FlyWP\StaticSite\Engine\Discovery\WordPressGateway;
11 use FlyWP\StaticSite\Engine\Discovery\WpGateway;
12
13 /**
14 * Turns change rows into the URLs of an incremental build.
15 *
16 * - A global row forces a full build. One query finds it before other rows are read.
17 * - For each changed post: the permalink, its <!--nextpage--> pages, its comment pages (comment changes),
18 * and all pages of its lists (terms, posts page, author, year, month, or the post type archive).
19 * - For any change: the home page, the main and comments feeds, and the sitemaps.
20 * - A list with more than MAX_LIST_PAGES pages forces a full build ("large_list").
21 * - Known limit: other pages that show a post (Latest Posts block, related posts, widgets with new posts)
22 * are not rebuilt. The "Full rebuild" button covers this.
23 *
24 * @since 1.8.0
25 */
26 class ChangeSetBuilder {
27
28 /**
29 * Rows and posts in one batch.
30 */
31 const BATCH_SIZE = 500;
32
33 /**
34 * Highest page count of one list in an incremental build.
35 */
36 const MAX_LIST_PAGES = 50;
37
38 /**
39 * @var ChangeStore
40 */
41 private $store;
42
43 /**
44 * @var WordPressGateway
45 */
46 private $wp;
47
48 /**
49 * @var string|null
50 */
51 private $origin_url;
52
53 /**
54 * URLs to rebuild, as keys.
55 *
56 * @var array<string, bool>
57 */
58 private $urls = [];
59
60 /**
61 * Deleted URLs, as keys.
62 *
63 * @var array<string, bool>
64 */
65 private $deleted = [];
66
67 /**
68 * List page 1 URL => page count.
69 *
70 * @var array<string, int>
71 */
72 private $lists = [];
73
74 /**
75 * @var bool
76 */
77 private $large_list = false;
78
79 /**
80 * Cached counts, by key.
81 *
82 * @var array<string, int>
83 */
84 private $counts = [];
85
86 /**
87 * @param ChangeStore|null $store Change rows. Null for the database table.
88 * @param WordPressGateway|null $wp Gateway. Null for WordPress.
89 * @param string|null $origin_url Origin URL that the engine fetches. Null to use the home URL.
90 */
91 public function __construct( ChangeStore $store = null, WordPressGateway $wp = null, $origin_url = null ) {
92 $this->wp = $wp ? $wp : new WpGateway();
93 $this->store = $store ? $store : new ChangeLog( $this->wp );
94 $this->origin_url = $origin_url;
95 }
96
97 /**
98 * Read the rows after $since_change_id, up to the highest ID at the start. Rows written during the build stay for the next build.
99 *
100 * @param int $since_change_id Highest row ID of the previous build.
101 *
102 * @return ChangeSet
103 */
104 public function build( $since_change_id = 0 ) {
105 $since = (int) $since_change_id;
106 $set = new ChangeSet();
107 $set->max_change_id = max( $since, (int) $this->store->max_id() );
108
109 if ( $set->max_change_id === $since ) {
110 return $set;
111 }
112
113 if ( $this->store->has_global( $since, $set->max_change_id ) ) {
114 $set->full_reason = 'global_change';
115
116 return $set;
117 }
118
119 $this->urls = [];
120 $this->deleted = [];
121 $this->lists = [];
122 $this->large_list = false;
123
124 $posts = [];
125 $terms = [];
126 $list_rows = [];
127 $comment_posts = [];
128 $after_id = $since;
129
130 do {
131 $rows = $this->store->rows( $after_id, $set->max_change_id, self::BATCH_SIZE );
132 $full_batch = count( $rows ) === self::BATCH_SIZE;
133
134 foreach ( $rows as $row ) {
135 $after_id = (int) $row['id'];
136 $url = (string) $row['url'];
137 $object_id = null === $row['object_id'] ? null : (int) $row['object_id'];
138
139 if ( 'delete' === $row['kind'] ) {
140 self::add_to( $this->deleted, $url );
141 }
142
143 if ( 'list' === $row['kind'] ) {
144 $list_rows[] = [ $row['object_type'], $object_id, $url ];
145 } elseif ( 'post' === $row['object_type'] ) {
146 self::add_to( $posts[ $object_id ], $url );
147
148 if ( 'comment' === $row['reason'] ) {
149 $comment_posts[ $object_id ] = true;
150 }
151 } elseif ( 'term' === $row['object_type'] ) {
152 self::add_to( $terms[ $object_id ], $url );
153 } elseif ( 'url' === $row['kind'] ) {
154 self::add_to( $this->urls, $url );
155 }
156 }
157 } while ( $full_batch );
158
159 if ( $after_id === $since ) {
160 return $set;
161 }
162
163 $this->add_site_urls();
164 $this->expand_posts( $posts, $comment_posts );
165 $this->expand_terms( $terms );
166
167 foreach ( $list_rows as $list ) {
168 $this->expand_list( $list[0], $list[1], $list[2] );
169 }
170
171 if ( $this->large_list ) {
172 $set->full_reason = 'large_list';
173
174 return $set;
175 }
176
177 $origin = new OriginUrl( $this->wp->home_url(), $this->origin_url );
178 $set->urls = self::rebase( $origin, array_keys( $this->urls ) );
179 $set->deleted_urls = array_values( array_diff( self::rebase( $origin, array_keys( $this->deleted ) ), $set->urls ) );
180
181 foreach ( $this->lists as $link => $pages ) {
182 $url = $origin->rebase( $link );
183
184 if ( null !== $url ) {
185 $set->lists[] = [
186 'url' => $url,
187 'pages' => $pages,
188 ];
189 }
190 }
191
192 return $set;
193 }
194
195 /**
196 * Pages that show every change: home, feeds and sitemaps.
197 *
198 * @return void
199 */
200 private function add_site_urls() {
201 self::add_to( $this->urls, $this->wp->home_url() );
202 self::add_to( $this->urls, $this->wp->feed_link() );
203 self::add_to( $this->urls, $this->wp->feed_link( 'comments_' ) );
204
205 foreach ( $this->wp->sitemap_urls() as $url ) {
206 self::add_to( $this->urls, $url );
207 }
208 }
209
210 /**
211 * @param array<int, array<string, bool>> $posts Post ID => URLs recorded for the post.
212 * @param array<int, bool> $comment_posts Post IDs with a comment change.
213 *
214 * @return void
215 */
216 private function expand_posts( array $posts, array $comment_posts ) {
217 $public_types = $this->wp->public_post_types();
218
219 foreach ( array_chunk( array_keys( $posts ), self::BATCH_SIZE ) as $post_ids ) {
220 $noindex = RobotsMeta::noindex_post_ids( $this->wp->seo_robots_meta( $post_ids ) );
221
222 foreach ( $post_ids as $post_id ) {
223 $post = $this->wp->post( $post_id );
224 $public = null !== $post && in_array( $post['post_type'], $public_types, true );
225
226 if ( $public && 'publish' === $post['status'] && ! PostTypeSource::is_excluded( $post, $noindex ) ) {
227 $this->add_post_pages( $post, isset( $comment_posts[ $post_id ] ) );
228 } else {
229 $this->deleted += (array) $posts[ $post_id ];
230 }
231
232 if ( $public ) {
233 foreach ( PostLists::for_post( $this->wp, $post ) as $list ) {
234 $this->expand_list( $list['type'], $list['id'], $list['url'] );
235 }
236 }
237 }
238 }
239 }
240
241 /**
242 * The permalink, the <!--nextpage--> pages, and the comment pages when comments changed.
243 *
244 * @param array{id:int, pages:int} $post Post.
245 * @param bool $comments True for a comment change.
246 *
247 * @return void
248 */
249 private function add_post_pages( array $post, $comments ) {
250 $permalink = $this->wp->permalink( $post['id'] );
251
252 if ( null === $permalink ) {
253 return;
254 }
255
256 self::add_to( $this->urls, $permalink );
257
258 $last_page = min( (int) ( $post['pages'] ?? 1 ), self::MAX_LIST_PAGES );
259
260 for ( $page = 2; $page <= $last_page; $page++ ) {
261 self::add_to( $this->urls, $this->wp->post_page_url( $permalink, $page ) );
262 }
263
264 if ( ! $comments || ! $this->wp->option( 'page_comments' ) ) {
265 return;
266 }
267
268 $count = $this->wp->approved_comment_count( $post['id'], (bool) $this->wp->option( 'thread_comments' ) );
269 $pages = Pagination::page_count( $count, $this->wp->option( 'comments_per_page' ) );
270
271 $last_page = min( $pages, self::MAX_LIST_PAGES );
272
273 for ( $page = 1; $pages > 1 && $page <= $last_page; $page++ ) {
274 self::add_to( $this->urls, $this->wp->comment_page_url( $permalink, $page ) );
275 }
276 }
277
278 /**
279 * @param array<int, array<string, bool>> $terms Term ID => URLs recorded for the term.
280 *
281 * @return void
282 */
283 private function expand_terms( array $terms ) {
284 $public_taxonomies = $this->wp->public_taxonomies();
285
286 foreach ( $terms as $term_id => $recorded ) {
287 $term = $this->wp->term( $term_id );
288 $link = null !== $term && in_array( $term['taxonomy'], $public_taxonomies, true ) ? $this->wp->term_link( $term_id, $term['taxonomy'] ) : null;
289
290 if ( null === $link ) {
291 $this->deleted += (array) $recorded;
292
293 continue;
294 }
295
296 $this->add_list( $link, $term['count'] );
297 }
298 }
299
300 /**
301 * Add all pages of a list. Counts come from the current data.
302 *
303 * @param string $type term, blog, user, date or archive.
304 * @param int|null $id Term ID, user ID, or year (YYYY) or month (YYYYMM).
305 * @param string|null $url Page 1 URL at change time.
306 *
307 * @return void
308 */
309 private function expand_list( $type, $id, $url ) {
310 if ( 'term' === $type ) {
311 $term = $this->wp->term( $id );
312
313 if ( null !== $term ) {
314 $this->add_list( $this->wp->term_link( $id, $term['taxonomy'] ), $term['count'] );
315 }
316 } elseif ( 'user' === $type ) {
317 $this->add_list( $this->wp->author_link( $id ), $this->count( 'author', $id ) );
318 } elseif ( 'blog' === $type ) {
319 $this->add_list( HomeSource::blog_url( $this->wp ), $this->count( 'type', 'post' ) );
320 } elseif ( 'date' === $type ) {
321 $year = $id > 9999 ? intdiv( $id, 100 ) : (int) $id;
322 $month = $id > 9999 ? $id % 100 : 0;
323 $link = $month > 0 ? $this->wp->month_link( $year, $month ) : $this->wp->year_link( $year );
324
325 $this->add_list( $link, $this->wp->date_post_count( $year, $month ) );
326 } elseif ( 'archive' === $type ) {
327 foreach ( $this->wp->public_post_types() as $post_type ) {
328 if ( 'post' !== $post_type && null !== $url && $this->wp->post_type_archive_link( $post_type ) === $url ) {
329 $this->add_list( $url, $this->count( 'type', $post_type ) );
330 }
331 }
332 }
333 }
334
335 /**
336 * @param string|null $link Page 1 URL.
337 * @param int $item_count Items in the list.
338 *
339 * @return void
340 */
341 private function add_list( $link, $item_count ) {
342 if ( null === $link ) {
343 return;
344 }
345
346 $pages = Pagination::page_count( $item_count, $this->wp->option( 'posts_per_page' ) );
347
348 if ( $pages > self::MAX_LIST_PAGES ) {
349 $this->large_list = true;
350
351 return;
352 }
353
354 $this->lists[ $link ] = $pages;
355
356 self::add_to( $this->urls, $link );
357
358 foreach ( Pagination::urls( $this->wp, $link, $item_count ) as $page ) {
359 self::add_to( $this->urls, $page->url );
360 }
361 }
362
363 /**
364 * @param string $kind "type" or "author".
365 * @param string|int $value Post type or user ID.
366 *
367 * @return int
368 */
369 private function count( $kind, $value ) {
370 $key = $kind . ':' . $value;
371
372 if ( ! isset( $this->counts[ $key ] ) ) {
373 $this->counts[ $key ] = 'type' === $kind ? $this->wp->published_count( $value ) : $this->wp->author_post_count( (int) $value );
374 }
375
376 return $this->counts[ $key ];
377 }
378
379 /**
380 * @param array<string, bool>|null $set URL set.
381 * @param string|null $url URL.
382 *
383 * @return void
384 */
385 private static function add_to( &$set, $url ) {
386 $set = (array) $set;
387
388 if ( is_string( $url ) && '' !== $url ) {
389 $set[ $url ] = true;
390 }
391 }
392
393 /**
394 * @param OriginUrl $origin Origin.
395 * @param string[] $urls URLs.
396 *
397 * @return string[] Unique URLs on the origin.
398 */
399 private static function rebase( OriginUrl $origin, array $urls ) {
400 return array_values( array_unique( array_filter( array_map( [ $origin, 'rebase' ], $urls ) ) ) );
401 }
402 }
403