| 1 |
<?php |
| 2 |
/** |
| 3 |
* The Lazy Loading DOM modifier. |
| 4 |
* |
| 5 |
* @package SolidWP\Performance |
| 6 |
*/ |
| 7 |
|
| 8 |
declare( strict_types=1 ); |
| 9 |
|
| 10 |
namespace SolidWP\Performance\Dom\Modifiers; |
| 11 |
|
| 12 |
use Closure; |
| 13 |
use InvalidArgumentException; |
| 14 |
use LogicException; |
| 15 |
use SolidWP\Performance\Assets\Asset; |
| 16 |
use SolidWP\Performance\Config\Config; |
| 17 |
use SolidWP\Performance\Dom\Contracts\Modifier; |
| 18 |
use SolidWP\Performance\Psr\Log\LoggerInterface; |
| 19 |
use SolidWP\Performance\Symfony\Component\DomCrawler\Crawler; |
| 20 |
|
| 21 |
/** |
| 22 |
* The Lazy Loading DOM modifier. |
| 23 |
* |
| 24 |
* @package SolidWP\Performance |
| 25 |
*/ |
| 26 |
final class Lazy_Load_Modifier implements Modifier { |
| 27 |
|
| 28 |
/** |
| 29 |
* Whether we found CSS background images to process and need to inject |
| 30 |
* our JS into the markup. |
| 31 |
* |
| 32 |
* @var bool |
| 33 |
*/ |
| 34 |
private bool $inject_script = false; |
| 35 |
|
| 36 |
/** |
| 37 |
* @var Config |
| 38 |
*/ |
| 39 |
private Config $config; |
| 40 |
|
| 41 |
/** |
| 42 |
* @var Asset |
| 43 |
*/ |
| 44 |
private Asset $asset; |
| 45 |
|
| 46 |
/** |
| 47 |
* @var LoggerInterface |
| 48 |
*/ |
| 49 |
private LoggerInterface $logger; |
| 50 |
|
| 51 |
/** |
| 52 |
* @param Config $config The config object. |
| 53 |
* @param Asset $asset The asset handler. |
| 54 |
* @param LoggerInterface $logger The logger. |
| 55 |
*/ |
| 56 |
public function __construct( Config $config, Asset $asset, LoggerInterface $logger ) { |
| 57 |
$this->config = $config; |
| 58 |
$this->asset = $asset; |
| 59 |
$this->logger = $logger; |
| 60 |
} |
| 61 |
|
| 62 |
/** |
| 63 |
* Whether this pipeline step is enabled. |
| 64 |
* |
| 65 |
* @return bool |
| 66 |
*/ |
| 67 |
public function is_enabled(): bool { |
| 68 |
return (bool) $this->config->get( 'page_cache.lazy_loading.enabled' ); |
| 69 |
} |
| 70 |
|
| 71 |
/** |
| 72 |
* Return true immediately if we're enabled, otherwise pass the current result to the next stage. |
| 73 |
* |
| 74 |
* @param bool $enabled Whether the previous stage is enabled. |
| 75 |
* @param Closure $next The closer passed to the next stage. |
| 76 |
* |
| 77 |
* @return bool |
| 78 |
*/ |
| 79 |
public function enabled( bool $enabled, Closure $next ): bool { |
| 80 |
return $this->is_enabled() ?: $next( $enabled ); |
| 81 |
} |
| 82 |
|
| 83 |
/** |
| 84 |
* Find elements that have a style tag which contains a `url()` data tag. |
| 85 |
* Add our custom data attributes and copy the entire style attribute content to another |
| 86 |
* data attribute in order to be swapped back when it enters the viewport to enable CSS background |
| 87 |
* lazy loading. |
| 88 |
* |
| 89 |
* @param Crawler $crawler The Symfony Crawler instance. |
| 90 |
* @param Closure $next The next closure to pass in the pipeline. |
| 91 |
* |
| 92 |
* @throws InvalidArgumentException When current node is empty. |
| 93 |
* @throws LogicException If the CssSelector Component is not available. |
| 94 |
* |
| 95 |
* @return Crawler |
| 96 |
*/ |
| 97 |
public function modify( Crawler $crawler, Closure $next ): Crawler { |
| 98 |
if ( ! $this->is_enabled() ) { |
| 99 |
return $next( $crawler ); |
| 100 |
} |
| 101 |
|
| 102 |
// Iterate through all elements with a style tag that contains a url() data type. |
| 103 |
$crawler->filter( '[style*="url("]' )->each( |
| 104 |
function ( Crawler $node ) { |
| 105 |
$current = $node->getNode( 0 ); |
| 106 |
|
| 107 |
$style = $node->attr( 'style' ); |
| 108 |
|
| 109 |
// Match CSS property:value pairs. |
| 110 |
preg_match_all( '/([\w-]+)\s*:\s*([^;]+)\s*;?/', $style, $matches, PREG_SET_ORDER ); |
| 111 |
|
| 112 |
$style_without_urls = ''; |
| 113 |
$style_with_urls = ''; |
| 114 |
|
| 115 |
foreach ( $matches as $match ) { |
| 116 |
$css_property = trim( $match[1] ); |
| 117 |
$css_value = trim( $match[2] ); |
| 118 |
|
| 119 |
$formatted_style = "$css_property:$css_value;"; |
| 120 |
|
| 121 |
if ( str_contains( $css_value, 'url(' ) ) { |
| 122 |
$style_with_urls .= $formatted_style; |
| 123 |
} else { |
| 124 |
$style_without_urls .= $formatted_style; |
| 125 |
} |
| 126 |
} |
| 127 |
|
| 128 |
// These must match our attributes from our lazy-load.js script. |
| 129 |
$current->setAttribute( 'style', $style_without_urls ); |
| 130 |
$current->setAttribute( 'data-solid-perf-style', $style_with_urls ); |
| 131 |
$current->setAttribute( 'data-solid-perf-trigger', 'viewport' ); |
| 132 |
$current->setAttribute( 'data-solid-perf-attrs', 'style' ); |
| 133 |
|
| 134 |
$this->inject_script = true; |
| 135 |
} |
| 136 |
); |
| 137 |
|
| 138 |
if ( $this->inject_script ) { |
| 139 |
$this->inject_lazy_loading_script( $crawler ); |
| 140 |
} |
| 141 |
|
| 142 |
return $next( $crawler ); |
| 143 |
} |
| 144 |
|
| 145 |
/** |
| 146 |
* Injects our lazy loading script tag just before the closing </body> tag. |
| 147 |
* |
| 148 |
* @param Crawler $crawler The crawler instance. |
| 149 |
* |
| 150 |
* @throws LogicException If the CssSelector Component is not available. |
| 151 |
* |
| 152 |
* @return void |
| 153 |
*/ |
| 154 |
private function inject_lazy_loading_script( Crawler $crawler ): void { |
| 155 |
$this->logger->debug( 'DOM Modifier: injecting lazyLoader.js before closing body tag' ); |
| 156 |
|
| 157 |
$body = $crawler->filter( 'body' ); |
| 158 |
|
| 159 |
if ( $body->count() < 0 ) { |
| 160 |
return; |
| 161 |
} |
| 162 |
|
| 163 |
// Build the script "src" attribute. |
| 164 |
$script_src = sprintf( |
| 165 |
'%s?ver=%s', |
| 166 |
$this->asset->get_url( 'build/lazyLoader.js' ), |
| 167 |
$this->asset->get_meta( 'build/lazyLoader' )['version'] ?? '1.0.0' |
| 168 |
); |
| 169 |
|
| 170 |
$script_node = $crawler->getNode( 0 )->ownerDocument->createElement( 'script' ); |
| 171 |
$script_node->setAttribute( 'src', esc_url( $script_src ) ); |
| 172 |
|
| 173 |
$body->getNode( 0 )->appendChild( $script_node ); |
| 174 |
} |
| 175 |
} |
| 176 |
|