Issues (1)

src/Enqueue.php (1 issue)

Labels
Severity
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * A chainable helper class for enqueuing scripts and styles.
7
 *
8
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
9
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
10
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
11
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
12
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
13
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
14
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
15
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
16
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
17
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
18
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
19
 *
20
 * @author Glynn Quelch <[email protected]>
21
 * @license http://www.opensource.org/licenses/mit-license.html  MIT License
22
 * @package PinkCrab\Enqueue
23
 */
24
25
namespace PinkCrab\Enqueue;
26
27
/**
28
 * WordPress Script and Style enqueuing class.
29
 *
30
 * @version 1.1.0
31
 * @author Glynn Quelch <[email protected]>
32
 */
33
class Enqueue {
34
35
	/**
36
	 * The handle to enqueue the script or style with.
37
	 * Also used for any localised variables.
38
	 *
39
	 * @var string
40
	 */
41
	protected $handle;
42
43
	/**
44
	 * The type of file to enqueue.
45
	 *
46
	 * @var string
47
	 */
48
	protected $type;
49
50
	/**
51
	 * The file location (URI)
52
	 *
53
	 * @var string
54
	 */
55
	protected $src;
56
57
	/**
58
	 * Dependencies which must be loaded prior.
59
	 *
60
	 * @var array<int, string>
61
	 */
62
	protected $deps = array();
63
64
	/**
65
	 * Version tag for file enqueued
66
	 *
67
	 * @var mixed
68
	 */
69
	protected $ver = false;
70
71
	/**
72
	 * Defines if script should be loaded in footer (true) or header (false)
73
	 *
74
	 * @var boolean
75
	 */
76
	protected $footer = true;
77
78
	/**
79
	 * Values to be localized when script enqueued.
80
	 *
81
	 * @var array<string, mixed>|null
82
	 */
83
	protected $localize = null;
84
85
	/**
86
	 * Defines if script should be parsed inline or enqueued.
87
	 * Please note this should only be used for simple and small JS files.
88
	 *
89
	 * @var boolean
90
	 */
91
	protected $inline = false;
92
93
	/**
94
	 * Style sheet which has been defined.
95
	 * Accepts media types like wp_enqueue_styles.
96
	 *
97
	 * @var string
98
	 */
99
	protected $media = 'all';
100
101
	/**
102
	 * All custom flags and attributes to add to the script and style tags
103
	 *
104
	 * @var array<string, string|null>
105
	 */
106
	protected $attributes = array();
107
108
	/**
109
	 * Denotes if being enqueued for a block.
110
	 *
111
	 * @var bool
112
	 */
113
	protected $for_block = false;
114
115
	/**
116
	 * Denotes the script type.
117
	 *
118
	 * @var string
119
	 */
120
	protected $script_type = 'text/javascript';
121
122
	/**
123
	 * Creates an Enqueue instance.
124
	 *
125
	 * @param string $handle
126
	 * @param string $type
127
	 */
128
	public function __construct( string $handle, string $type ) {
129
		$this->handle = $handle;
130
		$this->type   = $type;
131
	}
132
133
	/**
134
	 * Creates a static instance of the Enqueue class for a script.
135
	 *
136
	 * @param string $handle
137
	 * @return self
138
	 */
139
	public static function script( string $handle ): self {
140
		return new self( $handle, 'script' );
141
	}
142
143
	/**
144
	 * Creates a static instance of the Enqueue class for a style.
145
	 *
146
	 * @param string $handle
147
	 * @return self
148
	 */
149
	public static function style( string $handle ): self {
150
		return new self( $handle, 'style' );
151
	}
152
153
	/**
154
	 * Defined the SRC of the file.
155
	 *
156
	 * @param string $src
157
	 * @return self
158
	 */
159
	public function src( string $src ): self {
160
		$this->src = $src;
161
		return $this;
162
	}
163
164
	/**
165
	 * Defined the Dependencies of the enqueue.
166
	 *
167
	 * @param string ...$deps
168
	 * @return self
169
	 */
170
	public function deps( string ...$deps ): self {
171
		$this->deps = array_values( $deps );
172
		return $this;
173
	}
174
175
	/**
176
	 * Defined the version of the enqueue
177
	 *
178
	 * @param string $ver
179
	 * @return self
180
	 */
181
	public function ver( string $ver ): self {
182
		$this->ver = $ver;
183
		return $this;
184
	}
185
186
	/**
187
	 * Define the media type.
188
	 *
189
	 * @param string $media
190
	 * @return self
191
	 */
192
	public function media( string $media ): self {
193
		$this->media = $media;
194
		return $this;
195
	}
196
197
	/**
198
	 * DEPRECATED DUE TO TYPO
199
	 *
200
	 * see latest_version()
201
	 * @deprecated 1.3.0
202
	 */
203
	public function lastest_version():self {
204
		trigger_error( 'Method ' . __METHOD__ . ' is deprecated', E_USER_DEPRECATED ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
205
		return $this->latest_version();
206
	}
207
208
	/**
209
	 * Sets the version as last modified file time.
210
	 * Doesnt set the version if the fileheader can be read.
211
	 *
212
	 * @return self
213
	 */
214
	public function latest_version(): self {
215
		if ( $this->does_file_exist( $this->src ) ) {
216
217
			// If php8 or above set as bool, else int
218
			$associate = ( PHP_VERSION_ID >= 80000 ) ? true : 1;
219
220
			$headers = get_headers( $this->src, $associate );
0 ignored issues
show
It seems like $associate can also be of type integer; however, parameter $associative of get_headers() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

220
			$headers = get_headers( $this->src, /** @scrutinizer ignore-type */ $associate );
Loading history...
221
222
			if ( is_array( $headers )
223
			&& array_key_exists( 'Last-Modified', $headers )
224
			) {
225
				$this->ver = strtotime( $headers['Last-Modified'] );
226
			}
227
		}
228
		return $this;
229
	}
230
231
	/**
232
	 * Checks to see if a file exist using URL (not path).
233
	 *
234
	 * @param string $url The URL of the file being checked.
235
	 * @return boolean true if it does, false if it doesnt.
236
	 */
237
	private function does_file_exist( string $url ): bool {
238
		$ch = curl_init( $url );
239
		if ( ! $ch ) {
240
			return false;
241
		}
242
		curl_setopt( $ch, CURLOPT_NOBODY, true );
243
		curl_setopt( $ch, CURLOPT_TIMEOUT_MS, 50 );
244
		curl_exec( $ch );
245
		$http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
246
		curl_close( $ch );
247
		return $http_code === 200;
248
	}
249
250
	/**
251
	 * Should the script be called in the footer.
252
	 *
253
	 * @param boolean $footer
254
	 * @return self
255
	 */
256
	public function footer( bool $footer = true ): self {
257
		$this->footer = $footer;
258
		return $this;
259
	}
260
261
	/**
262
	 * Alias for footerfalse
263
	 *
264
	 * @return self
265
	 */
266
	public function header(): self {
267
		$this->footer = false;
268
		return $this;
269
	}
270
271
	/**
272
	 * Should the script be called in the inline.
273
	 *
274
	 * @param boolean $inline
275
	 * @return self
276
	 */
277
	public function inline( bool $inline = true ):self {
278
		$this->inline = $inline;
279
		return $this;
280
	}
281
282
	/**
283
	 * Pass any key => value pairs to be localised with the enqueue.
284
	 *
285
	 * @param array<string, mixed> $args
286
	 * @return self
287
	 */
288
	public function localize( array $args ): self {
289
		$this->localize = $args;
290
		return $this;
291
	}
292
293
	/**
294
	 * Adds a Flag (attribute with no value) to a script/style tag
295
	 *
296
	 * @param string $flag
297
	 * @return self
298
	 */
299
	public function flag( string $flag ): self {
300
		$this->attributes[ $flag ] = null;
301
		return $this;
302
	}
303
304
	/**
305
	 * Adds an attribute tto a script/style tag
306
	 *
307
	 * @param string $key
308
	 * @param string $value
309
	 * @return self
310
	 */
311
	public function attribute( string $key, string $value ): self {
312
		$this->attributes[ $key ] = $value;
313
		return $this;
314
	}
315
316
	/**
317
	 * Marks the script or style as deferred loaded.
318
	 *
319
	 * @return self
320
	 */
321
	public function defer(): self {
322
		// Remove ASYNC if set.
323
		if ( \array_key_exists( 'async', $this->attributes ) ) {
324
			unset( $this->attributes['async'] );
325
		}
326
327
		$this->attributes['defer'] = '';
328
		return $this;
329
	}
330
331
	/**
332
	 * Marks the script or style as async loaded.
333
	 *
334
	 * @return self
335
	 */
336
	public function async(): self {
337
		// Remove DEFER if set.
338
		if ( \array_key_exists( 'defer', $this->attributes ) ) {
339
			unset( $this->attributes['defer'] );
340
		}
341
342
		$this->attributes['async'] = '';
343
		return $this;
344
	}
345
346
	/**
347
	 * Set if being enqueued for a block.
348
	 *
349
	 * @param bool $for_block Denotes if being enqueued for a block.
350
	 * @return self
351
	 */
352
	public function for_block( bool $for_block = true ) : self {
353
		$this->for_block = $for_block;
354
		return $this;
355
	}
356
357
	/**
358
	 * Registers the file as either enqueued or inline parsed.
359
	 *
360
	 * @return void
361
	 */
362
	public function register(): void {
363
		if ( $this->type === 'script' ) {
364
			$this->register_script();
365
		}
366
367
		if ( $this->type === 'style' ) {
368
			$this->register_style();
369
		}
370
	}
371
372
	/**
373
	 * Regsiters the style.
374
	 *
375
	 * @return void
376
	 */
377
	private function register_style() {
378
379
		\wp_register_style(
380
			$this->handle,
381
			$this->src,
382
			$this->deps,
383
			$this->ver,
384
			$this->media
385
		);
386
		if ( false === $this->for_block ) {
387
			wp_enqueue_style( $this->handle );
388
		}
389
390
		$this->add_style_attributes();
391
392
	}
393
394
	/**
395
	 * Registers and enqueues or inlines the script, with any passed localised data.
396
	 *
397
	 * @return void
398
	 */
399
	private function register_script() {
400
401
		\wp_register_script(
402
			$this->handle,
403
			$this->inline === true ? '' : $this->src,
404
			$this->deps,
405
			$this->ver,
406
			$this->footer
407
		);
408
409
		// Maybe add as an inline script.
410
		if ( $this->inline && $this->does_file_exist( $this->src ) ) {
411
			\wp_add_inline_script( $this->handle, file_get_contents( $this->src ) ?: '' );
412
		}
413
414
		// Localize all values if defined.
415
		if ( ! empty( $this->localize ) ) {
416
			\wp_localize_script( $this->handle, $this->handle, $this->localize );
417
		}
418
419
		// Enqueue file if not used for a block.
420
		if ( false === $this->for_block ) {
421
			\wp_enqueue_script( $this->handle );
422
		}
423
424
		$this->add_script_attributes();
425
	}
426
427
	/**
428
	 * Adds any additional attributes to a script.
429
	 *
430
	 * @return void
431
	 */
432
	private function add_script_attributes(): void {
433
434
		$attributes = $this->get_script_attributes();
435
436
		// Bail if we have no attributes.
437
		if ( 0 === count( $this->get_attributes() ) && $this->script_type === 'text/javascript' ) {
438
			return;
439
		}
440
441
		// Add to any scripts.
442
		add_filter(
443
			'script_loader_tag',
444
			function( string $tag, string $handle, string $source ) use ( $attributes ): string {
445
				// Bail if not our script.
446
				if ( $this->handle !== $handle ) {
447
					return $tag;
448
				}
449
				return sprintf( '<script type="%s" src="%s" %s></script>', $this->script_type, $source, join( ' ', $attributes ) ); //phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
450
			},
451
			1,
452
			3
453
		);
454
	}
455
456
	/**
457
	 * Adds the ID attribute if not set for script and script type is not text/javascript.
458
	 *
459
	 * @return string[]
460
	 */
461
	private function get_script_attributes(): array {
462
		$attributes = $this->get_attributes();
463
		// Loop through and look for any that start with 'id='
464
		foreach ( $attributes as $key => $value ) {
465
			if ( \strpos( $value, 'id=' ) === 0 ) {
466
				return $attributes;
467
			}
468
		}
469
470
		// Add to attributes
471
		$attributes[] = \sprintf( "id='%s'", "{$this->handle}-js" );
472
		return $attributes;
473
	}
474
475
	/**
476
	 * Adds any additional attributes to a style.
477
	 *
478
	 * @return void
479
	 */
480
	private function add_style_attributes(): void {
481
482
		$attributes = $this->get_attributes();
483
484
		// Bail if we have no attributes.
485
		if ( 0 === count( $attributes ) ) {
486
			return;
487
		}
488
489
		// Add to any relevant styles.
490
		add_filter(
491
			'style_loader_tag',
492
			function( string $tag, string $handle, string $href, string $media ) use ( $attributes ): string {
493
				// Bail if not our script.
494
				if ( $this->handle !== $handle ) {
495
					return $tag;
496
				}
497
				return sprintf(
498
					'<link rel="stylesheet" id="%s-css" href="%s" type="text/css" media="%s" %s>', //phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet
499
					$handle,
500
					$href,
501
					$media,
502
					join( ' ', $attributes )
503
				);
504
			},
505
			1,
506
			4
507
		);
508
	}
509
510
	/**
511
	 * Set denotes the script type.
512
	 *
513
	 * @param string $script_type  Denotes the script type.
514
	 * @return self
515
	 */
516
	public function script_type( string $script_type ) {
517
		$this->script_type = $script_type;
518
		return $this;
519
	}
520
521
	/**
522
	 * Gets all attributes mapped as HTML attributes.
523
	 *
524
	 * @return string[]
525
	 */
526
	private function get_attributes():array {
527
		return array_map(
528
			function( string $key, ?string $value ): string {
529
				return null === $value
530
					? "{$key}"
531
					: "{$key}='{$value}'";
532
			},
533
			array_keys( $this->attributes ),
534
			$this->attributes
535
		);
536
	}
537
538
}
539