PluginProbe
Image Optimizer – Compress Images and Convert to WebP or AVIF / 1.7.7
Image Optimizer – Compress Images and Convert to WebP or AVIF v1.7.7
1.7.7 1.7.6 1.7.5 1.7.4 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.2.0 1.2.1 1.3.0 1.4.0 1.4.1 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 All 33 releases
image-optimization / modules / optimization / classes / bulk-optimization / bulk-optimization-queue.php

bulk-optimization-queue.php in Image Optimizer – Compress Images and Convert to WebP or AVIF 1.7.7, at modules/optimization/classes/bulk-optimization/bulk-optimization-queue.php

408 lines 10.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace ImageOptimization\Modules\Optimization\Classes\Bulk_Optimization;
4
5 use ImageOptimization\Classes\Async_Operation\{
6 Async_Operation,
7 Async_Operation_Hook,
8 Exceptions\Async_Operation_Exception,
9 Queries\Image_Optimization_Operation_Query,
10 };
11
12 use ImageOptimization\Classes\Image\{
13 Image_Meta,
14 Image_Optimization_Error_Type,
15 Image_Status,
16 WP_Image_Meta,
17 Exceptions\Invalid_Image_Exception,
18 };
19
20 use ImageOptimization\Classes\Logger;
21 use TypeError;
22
23 // @codeCoverageIgnoreStart
24 if ( ! defined( 'ABSPATH' ) ) {
25 exit; // Exit if accessed directly.
26 }
27 // @codeCoverageIgnoreEnd
28
29 final class Bulk_Optimization_Queue {
30 private const OPTION_PREFIX = 'image_optimizer_bulk_queue_';
31 private const MAX_RETRIES = 3;
32
33 private const INITIAL_QUEUE_VALUE = [
34 'operation_id' => null,
35 'type' => null,
36 'bulk_token' => null,
37 'token_expires_at' => null,
38 'max_batch_size' => null, // Maximum batch size that successfully obtained a token
39 'images_optimized_with_current_token' => 0, // Counter for current token usage
40 'created_at' => null,
41 'status' => Bulk_Optimization_Queue_Status::PENDING,
42 'images' => [], // Array of ['id' => int, 'status' => 'pending'|'completed'|'failed']
43 'stats' => [
44 'total' => 0,
45 'completed' => 0,
46 'failed' => 0,
47 'pending' => 0,
48 ],
49 'current_image_id' => null,
50 ];
51
52 private string $type;
53 private array $queue_data;
54
55 public function get_operation_id(): ?string {
56 if ( $this->exists() && empty( $this->queue_data['operation_id'] ) ) {
57 $this->queue_data['operation_id'] = wp_generate_password( 10, false );
58 $this->save();
59 }
60
61 return $this->queue_data['operation_id'];
62 }
63
64 public function get_type(): string {
65 return $this->type;
66 }
67
68 public function get_bulk_token(): ?string {
69 return $this->queue_data['bulk_token'];
70 }
71
72 public function get_status(): string {
73 return $this->queue_data['status'];
74 }
75
76 public function get_images(): array {
77 return $this->queue_data['images'];
78 }
79
80 public function get_image_ids(): array {
81 return array_column( $this->queue_data['images'], 'id' );
82 }
83
84 public function get_images_by_status( string $status ): array {
85 if ( ! in_array( $status, Bulk_Optimization_Queue_Status::get_values(), true ) ) {
86 Logger::error( "Status $status is not a part of Bulk_Optimization_Queue_Status values" );
87
88 throw new TypeError( esc_html( "Status $status is not a part of Bulk_Optimization_Queue_Status values" ) );
89 }
90
91 return array_filter(
92 $this->queue_data['images'],
93 function ( $image ) use ( $status ) {
94 return $image['status'] === $status;
95 }
96 );
97 }
98
99 public function get_stats(): array {
100 return $this->queue_data['stats'];
101 }
102
103 public function get_current_image_id(): ?int {
104 return $this->queue_data['current_image_id'];
105 }
106
107 public function set_operation_id( string $id ): self {
108 $this->queue_data['operation_id'] = $id;
109
110 return $this;
111 }
112
113 public function set_bulk_token( string $token, int $expires_at, int $batch_size = null ): self {
114 $this->queue_data['bulk_token'] = $token;
115 $this->queue_data['token_expires_at'] = $expires_at;
116
117 // Update max batch size if provided and larger than current
118 if ( null !== $batch_size ) {
119 if ( null === $this->queue_data['max_batch_size'] || $batch_size > $this->queue_data['max_batch_size'] ) {
120 $this->queue_data['max_batch_size'] = $batch_size;
121 }
122 }
123
124 // Reset counter when new token is set
125 $this->queue_data['images_optimized_with_current_token'] = 0;
126
127 return $this;
128 }
129
130 public function get_max_batch_size(): ?int {
131 return $this->queue_data['max_batch_size'];
132 }
133
134 public function increment_optimized_counter(): self {
135 $this->queue_data['images_optimized_with_current_token']++;
136
137 return $this;
138 }
139
140 public function should_refresh_token(): bool {
141 if ( $this->is_token_expiring_soon() ) {
142 return true;
143 }
144
145 // Check if we've exhausted the current batch quota
146 $max_batch = $this->queue_data['max_batch_size'];
147 $optimized_count = $this->queue_data['images_optimized_with_current_token'];
148
149 if ( null !== $max_batch && $optimized_count >= $max_batch ) {
150 return true;
151 }
152
153 // Check if we have enough quota for the next pending image
154 if ( null !== $max_batch ) {
155 $next_image_id = $this->get_next_image();
156
157 if ( $next_image_id ) {
158 try {
159 $wp_meta = new WP_Image_Meta( $next_image_id );
160 $sizes_count = count( $wp_meta->get_size_keys() );
161 $remaining_quota = $max_batch - $optimized_count;
162
163 if ( $sizes_count > $remaining_quota ) {
164 return true;
165 }
166 } catch ( Invalid_Image_Exception $e ) {
167 // If we can't get image meta, continue with current token
168 return false;
169 }
170 }
171 }
172
173 return false;
174 }
175
176 public function set_status( string $status ): self {
177 if ( ! in_array( $status, Bulk_Optimization_Queue_Status::get_values(), true ) ) {
178 Logger::error( "Status $status is not a part of Bulk_Optimization_Queue_Status values" );
179
180 throw new TypeError( esc_html( "Status $status is not a part of Bulk_Optimization_Queue_Status values" ) );
181 }
182
183 $this->queue_data['status'] = $status;
184
185 return $this;
186 }
187
188 public function set_current_image_id( ?int $id ): self {
189 $this->queue_data['current_image_id'] = $id;
190
191 return $this;
192 }
193
194 public function add_images( array $image_ids ): self {
195 $existing_ids = array_column( $this->queue_data['images'], 'id' );
196
197 foreach ( $image_ids as $image_id ) {
198 if ( in_array( $image_id, $existing_ids, true ) ) {
199 continue;
200 }
201
202 $this->queue_data['images'][] = [
203 'id' => $image_id,
204 'status' => Bulk_Optimization_Queue_Status::PENDING,
205 ];
206
207 $existing_ids[] = $image_id;
208 }
209
210 $this->update_stats();
211
212 return $this;
213 }
214
215 public function get_next_image(): ?int {
216 $pending_images = $this->get_images_by_status( Bulk_Optimization_Queue_Status::PENDING );
217
218 if ( empty( $pending_images ) ) {
219 return null;
220 }
221
222 $first_image = reset( $pending_images );
223
224 return $first_image['id'];
225 }
226
227 public function mark_image_completed( int $image_id ): self {
228 foreach ( $this->queue_data['images'] as &$image ) {
229 if ( $image['id'] === $image_id ) {
230 $image['status'] = Bulk_Optimization_Queue_Status::COMPLETED;
231 break;
232 }
233 }
234
235 unset( $image );
236
237 ( new Image_Meta( $image_id ) )
238 ->set_retry_count( null )
239 ->save();
240
241 $this->update_stats();
242
243 return $this;
244 }
245
246 public function mark_image_failed( int $image_id ): self {
247 $meta = new Image_Meta( $image_id );
248 $retry_count = $meta->get_retry_count() ?? 0;
249 $retry_count++;
250
251 $is_reoptimization = Bulk_Optimization_Queue_Type::REOPTIMIZATION === $this->type;
252
253 // Update Image_Meta with failure and increment retry count
254 $meta->set_status(
255 $is_reoptimization
256 ? Image_Status::REOPTIMIZING_FAILED
257 : Image_Status::OPTIMIZATION_FAILED
258 )
259 ->set_retry_count( $retry_count )
260 ->save();
261
262 // Check if we should retry or mark as permanently failed
263 if ( $retry_count >= self::MAX_RETRIES ) {
264 // Mark as permanently failed in queue
265 foreach ( $this->queue_data['images'] as &$image ) {
266 if ( $image['id'] === $image_id ) {
267 $image['status'] = Bulk_Optimization_Queue_Status::FAILED;
268 break;
269 }
270 }
271 unset( $image ); // Break the reference
272
273 $meta
274 ->set_error_type( Image_Optimization_Error_Type::GENERIC )
275 ->save();
276 }
277
278 $this->update_stats();
279
280 return $this;
281 }
282
283 public function is_empty(): bool {
284 return empty( $this->queue_data['images'] );
285 }
286
287 public function has_more_images(): bool {
288 return ! empty( $this->get_images_by_status( Bulk_Optimization_Queue_Status::PENDING ) );
289 }
290
291 public function is_token_expired(): bool {
292 if ( ! $this->queue_data['token_expires_at'] ) {
293 return true;
294 }
295
296 return time() >= $this->queue_data['token_expires_at'];
297 }
298
299 public function is_token_expiring_soon(): bool {
300 $buffer_seconds = 5 * MINUTE_IN_SECONDS;
301
302 if ( ! $this->queue_data['token_expires_at'] ) {
303 return true;
304 }
305
306 return time() >= ( $this->queue_data['token_expires_at'] - $buffer_seconds );
307 }
308
309 public function save(): self {
310 update_option( $this->get_option_name(), $this->queue_data, false );
311
312 return $this;
313 }
314
315 public function delete(): bool {
316 $this->cancel_scheduled_actions();
317
318 return delete_option( $this->get_option_name() );
319 }
320
321 /**
322 * Cancels any scheduled actions associated with this queue.
323 */
324 private function cancel_scheduled_actions(): void {
325 $operation_id = $this->get_operation_id();
326
327 if ( empty( $operation_id ) ) {
328 return;
329 }
330
331 $hook = Bulk_Optimization_Queue_Type::OPTIMIZATION === $this->type
332 ? Async_Operation_Hook::OPTIMIZE_BULK
333 : Async_Operation_Hook::REOPTIMIZE_BULK;
334
335 $query = ( new Image_Optimization_Operation_Query() )
336 ->set_hook( $hook )
337 ->set_bulk_operation_id( $operation_id )
338 ->return_ids()
339 ->set_limit( -1 );
340
341 try {
342 $operation_ids = Async_Operation::get( $query );
343 Async_Operation::remove( $operation_ids );
344 } catch ( Async_Operation_Exception $aee ) {
345 Logger::error( "Error while removing redundant actions for the operation `{$operation_id}`" );
346 }
347 }
348
349 public function exists(): bool {
350 return false !== get_option( $this->get_option_name(), false );
351 }
352
353 public function __construct( string $type ) {
354 if ( ! in_array( $type, Bulk_Optimization_Queue_Type::get_values(), true ) ) {
355 Logger::error( "Type $type is not a part of Bulk_Optimization_Queue_Type values" );
356
357 throw new TypeError( esc_html( "Type $type is not a part of Bulk_Optimization_Queue_Type values" ) );
358 }
359
360 $this->type = $type;
361 $this->query_queue();
362 }
363
364 private function query_queue(): void {
365 $queue = get_option( $this->get_option_name(), false );
366 $this->queue_data = $queue
367 ? array_replace_recursive( self::INITIAL_QUEUE_VALUE, $queue )
368 : self::INITIAL_QUEUE_VALUE;
369
370 if ( ! $this->queue_data['type'] ) {
371 $this->queue_data['type'] = $this->type;
372 }
373
374 if ( ! $this->queue_data['created_at'] ) {
375 $this->queue_data['created_at'] = time();
376 }
377 }
378
379 private function get_option_name(): string {
380 return self::OPTION_PREFIX . $this->type;
381 }
382
383 private function update_stats(): void {
384 $completed = 0;
385 $failed = 0;
386 $pending = 0;
387
388 foreach ( $this->queue_data['images'] as $image ) {
389 switch ( $image['status'] ) {
390 case Bulk_Optimization_Queue_Status::COMPLETED:
391 $completed++;
392 break;
393 case Bulk_Optimization_Queue_Status::FAILED:
394 $failed++;
395 break;
396 case Bulk_Optimization_Queue_Status::PENDING:
397 $pending++;
398 break;
399 }
400 }
401
402 $this->queue_data['stats']['total'] = count( $this->queue_data['images'] );
403 $this->queue_data['stats']['completed'] = $completed;
404 $this->queue_data['stats']['failed'] = $failed;
405 $this->queue_data['stats']['pending'] = $pending;
406 }
407 }
408