Pink-Crab/Enqueue

View on GitHub
src/Enqueue.php

Summary

Maintainability
A
3 hrs
Test Coverage
<?php

declare(strict_types=1);

/**
 * A chainable helper class for enqueuing scripts and styles.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @author Glynn Quelch <glynn.quelch@gmail.com>
 * @license http://www.opensource.org/licenses/mit-license.html  MIT License
 * @package PinkCrab\Enqueue
 */

namespace PinkCrab\Enqueue;

/**
 * WordPress Script and Style enqueuing class.
 *
 * @version 1.1.0
 * @author Glynn Quelch <glynn.quelch@gmail.com>
 */
class Enqueue {

    /**
     * The handle to enqueue the script or style with.
     * Also used for any localised variables.
     *
     * @var string
     */
    protected $handle;

    /**
     * The type of file to enqueue.
     *
     * @var string
     */
    protected $type;

    /**
     * The file location (URI)
     *
     * @var string
     */
    protected $src;

    /**
     * Dependencies which must be loaded prior.
     *
     * @var array<int, string>
     */
    protected $deps = array();

    /**
     * Version tag for file enqueued
     *
     * @var mixed
     */
    protected $ver = false;

    /**
     * Defines if script should be loaded in footer (true) or header (false)
     *
     * @var boolean
     */
    protected $footer = true;

    /**
     * Values to be localized when script enqueued.
     *
     * @var array<string, mixed>|null
     */
    protected $localize = null;

    /**
     * Defines if script should be parsed inline or enqueued.
     * Please note this should only be used for simple and small JS files.
     *
     * @var boolean
     */
    protected $inline = false;

    /**
     * Style sheet which has been defined.
     * Accepts media types like wp_enqueue_styles.
     *
     * @var string
     */
    protected $media = 'all';

    /**
     * All custom flags and attributes to add to the script and style tags
     *
     * @var array<string, string|null>
     */
    protected $attributes = array();

    /**
     * Denotes if being enqueued for a block.
     *
     * @var bool
     */
    protected $for_block = false;

    /**
     * Denotes the script type.
     *
     * @var string
     */
    protected $script_type = 'text/javascript';

    /**
     * Creates an Enqueue instance.
     *
     * @param string $handle
     * @param string $type
     */
    public function __construct( string $handle, string $type ) {
        $this->handle = $handle;
        $this->type   = $type;
    }

    /**
     * Creates a static instance of the Enqueue class for a script.
     *
     * @param string $handle
     * @return self
     */
    public static function script( string $handle ): self {
        return new self( $handle, 'script' );
    }

    /**
     * Creates a static instance of the Enqueue class for a style.
     *
     * @param string $handle
     * @return self
     */
    public static function style( string $handle ): self {
        return new self( $handle, 'style' );
    }

    /**
     * Defined the SRC of the file.
     *
     * @param string $src
     * @return self
     */
    public function src( string $src ): self {
        $this->src = $src;
        return $this;
    }

    /**
     * Defined the Dependencies of the enqueue.
     *
     * @param string ...$deps
     * @return self
     */
    public function deps( string ...$deps ): self {
        $this->deps = array_values( $deps );
        return $this;
    }

    /**
     * Defined the version of the enqueue
     *
     * @param string $ver
     * @return self
     */
    public function ver( string $ver ): self {
        $this->ver = $ver;
        return $this;
    }

    /**
     * Define the media type.
     *
     * @param string $media
     * @return self
     */
    public function media( string $media ): self {
        $this->media = $media;
        return $this;
    }

    /**
     * DEPRECATED DUE TO TYPO
     *
     * see latest_version()
     * @deprecated 1.3.0
     */
    public function lastest_version():self {
        trigger_error( 'Method ' . __METHOD__ . ' is deprecated', E_USER_DEPRECATED ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
        return $this->latest_version();
    }

    /**
     * Sets the version as last modified file time.
     * Doesnt set the version if the fileheader can be read.
     *
     * @return self
     */
    public function latest_version(): self {
        if ( $this->does_file_exist( $this->src ) ) {

            // If php8 or above set as bool, else int
            $associate = ( PHP_VERSION_ID >= 80000 ) ? true : 1;

            $headers = get_headers( $this->src, $associate );

            if ( is_array( $headers )
            && array_key_exists( 'Last-Modified', $headers )
            ) {
                $this->ver = strtotime( $headers['Last-Modified'] );
            }
        }
        return $this;
    }

    /**
     * Checks to see if a file exist using URL (not path).
     *
     * @param string $url The URL of the file being checked.
     * @return boolean true if it does, false if it doesnt.
     */
    private function does_file_exist( string $url ): bool {
        $ch = curl_init( $url );
        if ( ! $ch ) {
            return false;
        }
        curl_setopt( $ch, CURLOPT_NOBODY, true );
        curl_setopt( $ch, CURLOPT_TIMEOUT_MS, 50 );
        curl_exec( $ch );
        $http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
        curl_close( $ch );
        return $http_code === 200;
    }

    /**
     * Should the script be called in the footer.
     *
     * @param boolean $footer
     * @return self
     */
    public function footer( bool $footer = true ): self {
        $this->footer = $footer;
        return $this;
    }

    /**
     * Alias for footerfalse
     *
     * @return self
     */
    public function header(): self {
        $this->footer = false;
        return $this;
    }

    /**
     * Should the script be called in the inline.
     *
     * @param boolean $inline
     * @return self
     */
    public function inline( bool $inline = true ):self {
        $this->inline = $inline;
        return $this;
    }

    /**
     * Pass any key => value pairs to be localised with the enqueue.
     *
     * @param array<string, mixed> $args
     * @return self
     */
    public function localize( array $args ): self {
        $this->localize = $args;
        return $this;
    }

    /**
     * Adds a Flag (attribute with no value) to a script/style tag
     *
     * @param string $flag
     * @return self
     */
    public function flag( string $flag ): self {
        $this->attributes[ $flag ] = null;
        return $this;
    }

    /**
     * Adds an attribute tto a script/style tag
     *
     * @param string $key
     * @param string $value
     * @return self
     */
    public function attribute( string $key, string $value ): self {
        $this->attributes[ $key ] = $value;
        return $this;
    }

    /**
     * Marks the script or style as deferred loaded.
     *
     * @return self
     */
    public function defer(): self {
        // Remove ASYNC if set.
        if ( \array_key_exists( 'async', $this->attributes ) ) {
            unset( $this->attributes['async'] );
        }

        $this->attributes['defer'] = '';
        return $this;
    }

    /**
     * Marks the script or style as async loaded.
     *
     * @return self
     */
    public function async(): self {
        // Remove DEFER if set.
        if ( \array_key_exists( 'defer', $this->attributes ) ) {
            unset( $this->attributes['defer'] );
        }

        $this->attributes['async'] = '';
        return $this;
    }

    /**
     * Set if being enqueued for a block.
     *
     * @param bool $for_block Denotes if being enqueued for a block.
     * @return self
     */
    public function for_block( bool $for_block = true ) : self {
        $this->for_block = $for_block;
        return $this;
    }

    /**
     * Registers the file as either enqueued or inline parsed.
     *
     * @return void
     */
    public function register(): void {
        if ( $this->type === 'script' ) {
            $this->register_script();
        }

        if ( $this->type === 'style' ) {
            $this->register_style();
        }
    }

    /**
     * Regsiters the style.
     *
     * @return void
     */
    private function register_style() {

        \wp_register_style(
            $this->handle,
            $this->src,
            $this->deps,
            $this->ver,
            $this->media
        );
        if ( false === $this->for_block ) {
            wp_enqueue_style( $this->handle );
        }

        $this->add_style_attributes();

    }

    /**
     * Registers and enqueues or inlines the script, with any passed localised data.
     *
     * @return void
     */
    private function register_script() {

        \wp_register_script(
            $this->handle,
            $this->inline === true ? '' : $this->src,
            $this->deps,
            $this->ver,
            $this->footer
        );

        // Maybe add as an inline script.
        if ( $this->inline && $this->does_file_exist( $this->src ) ) {
            \wp_add_inline_script( $this->handle, file_get_contents( $this->src ) ?: '' );
        }

        // Localize all values if defined.
        if ( ! empty( $this->localize ) ) {
            \wp_localize_script( $this->handle, $this->handle, $this->localize );
        }

        // Enqueue file if not used for a block.
        if ( false === $this->for_block ) {
            \wp_enqueue_script( $this->handle );
        }

        $this->add_script_attributes();
    }

    /**
     * Adds any additional attributes to a script.
     *
     * @return void
     */
    private function add_script_attributes(): void {

        $attributes = $this->get_script_attributes();

        // Bail if we have no attributes.
        if ( 0 === count( $this->get_attributes() ) && $this->script_type === 'text/javascript' ) {
            return;
        }

        // Add to any scripts.
        add_filter(
            'script_loader_tag',
            function( string $tag, string $handle, string $source ) use ( $attributes ): string {
                // Bail if not our script.
                if ( $this->handle !== $handle ) {
                    return $tag;
                }
                return sprintf( '<script type="%s" src="%s" %s></script>', $this->script_type, $source, join( ' ', $attributes ) ); //phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
            },
            1,
            3
        );
    }

    /**
     * Adds the ID attribute if not set for script and script type is not text/javascript.
     *
     * @return string[]
     */
    private function get_script_attributes(): array {
        $attributes = $this->get_attributes();
        // Loop through and look for any that start with 'id='
        foreach ( $attributes as $key => $value ) {
            if ( \strpos( $value, 'id=' ) === 0 ) {
                return $attributes;
            }
        }

        // Add to attributes
        $attributes[] = \sprintf( "id='%s'", "{$this->handle}-js" );
        return $attributes;
    }

    /**
     * Adds any additional attributes to a style.
     *
     * @return void
     */
    private function add_style_attributes(): void {

        $attributes = $this->get_attributes();

        // Bail if we have no attributes.
        if ( 0 === count( $attributes ) ) {
            return;
        }

        // Add to any relevant styles.
        add_filter(
            'style_loader_tag',
            function( string $tag, string $handle, string $href, string $media ) use ( $attributes ): string {
                // Bail if not our script.
                if ( $this->handle !== $handle ) {
                    return $tag;
                }
                return sprintf(
                    '<link rel="stylesheet" id="%s-css" href="%s" type="text/css" media="%s" %s>', //phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet
                    $handle,
                    $href,
                    $media,
                    join( ' ', $attributes )
                );
            },
            1,
            4
        );
    }

    /**
     * Set denotes the script type.
     *
     * @param string $script_type  Denotes the script type.
     * @return self
     */
    public function script_type( string $script_type ) {
        $this->script_type = $script_type;
        return $this;
    }

    /**
     * Gets all attributes mapped as HTML attributes.
     *
     * @return string[]
     */
    private function get_attributes():array {
        return array_map(
            function( string $key, ?string $value ): string {
                return null === $value
                    ? "{$key}"
                    : "{$key}='{$value}'";
            },
            array_keys( $this->attributes ),
            $this->attributes
        );
    }

}