diff --git a/CHANGELOG.md b/CHANGELOG.md index 833084c..95cce70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file. This projec to [Semantic Versioning] (http://semver.org/). For change log format, use [Keep a Changelog] (http://keepachangelog.com/). +## [2.2.0] - 2022-02-11 + +### Added + +- Tests for `preload` function + +### Changed + +- `entryPoints()` accept dynamic options +- `preload()` accept dynamic options + ## [2.1.0] - 2022-01-25 ### Changed diff --git a/src/Extension/AssetRuntimeExtension.php b/src/Extension/AssetRuntimeExtension.php index 4ffccf7..27b4caf 100644 --- a/src/Extension/AssetRuntimeExtension.php +++ b/src/Extension/AssetRuntimeExtension.php @@ -115,16 +115,12 @@ public function entryPoints( $entryPoint, array_merge(['as' => 'script'], $preloadOptions) ); + unset($options['preload']); } - // Defer/Async? - $deferOrAsync = ($options['defer'] ?? false) === true ? ' defer' : ''; - $deferOrAsync .= ($options['async'] ?? false) === true ? ' async' : ''; - $output .= sprintf( - '', - strip_tags($entryPoint), - $deferOrAsync + '', + $this->attributes(array_replace($options, ['src' => $entryPoint])), ) . PHP_EOL; break; case 'css': @@ -133,9 +129,18 @@ public function entryPoints( $entryPoint, array_merge(['as' => 'style'], $preloadOptions) ); + unset($options['preload']); } - $output .= sprintf('', strip_tags($entryPoint)) . PHP_EOL; + $output .= sprintf( + '', + $this->attributes( + array_replace( + $options, + ['rel' => 'stylesheet', 'href' => $entryPoint] + ) + ), + ) . PHP_EOL; break; } } @@ -166,6 +171,38 @@ public function entryPointsList(string|array $entry, ?string $type = null): arra } } + /** + * Make attributes. + * + * @param $attrs + * @param string|null $prefix + * + * @return string + */ + private function attributes($attrs, ?string $prefix = null): string + { + $output = ''; + + foreach ($attrs as $key => $value) { + if (null === $value || false === $value) { + continue; + } + + if (!empty($prefix)) { + $key = $prefix . '-' . $key; + } + + if (is_array($value)) { + $output .= $this->attributes($value, $key); + continue; + } + + $output .= ' ' . $key . (true !== $value ? '="' . htmlspecialchars($value) . '"' : ''); + } + + return $output; + } + /** * Function preload to pre loading of request for HTTP 2 protocol. * @@ -176,42 +213,35 @@ public function entryPointsList(string|array $entry, ?string $type = null): arra */ public function preload(string $link, array $parameters = []): string { - $push = !(!empty($parameters['nopush']) && $parameters['nopush'] == true); - - if (true === $push && in_array(md5($link), $this->h2pushCache)) { + if (($push = (false === ($parameters['nopush'] ?? false))) && in_array(md5($link), $this->h2pushCache)) { return $link; } $header = sprintf('Link: <%s>; rel=preload', $link); - // as - if (!empty($parameters['as'])) { - $header = sprintf('%s; as=%s', $header, $parameters['as']); - } - // type - if (!empty($parameters['type'])) { - $header = sprintf('%s; type=%s', $header, $parameters['as']); - } - // crossorigin - if (!empty($parameters['crossorigin']) && $parameters['crossorigin'] == true) { - $header .= '; crossorigin'; - } - // nopush - if (!$push) { - $header .= '; nopush'; + foreach ($parameters as $key => $value) { + if (!is_scalar($value)) { + continue; + } + + if (false === $value) { + continue; + } + + $header .= '; ' . $key . (true !== $value ? '=' . $value : ''); } - if (true === headers_sent()) { + if (true === $this->isHeadersSent()) { return $link; } - header($header, false); + $this->sendHeader($header, false); // Cache if ($push) { $this->h2pushCache[] = md5($link); - setcookie( + $this->setCookie( sprintf('%s[%s]', self::H2PUSH_CACHE_COOKIE, md5($link)), '1', [ @@ -227,4 +257,41 @@ public function preload(string $link, array $parameters = []): string return $link; } + + /** + * Is headers sent? + * + * @return bool + */ + public function isHeadersSent(): bool + { + return headers_sent(); + } + + /** + * Send header. + * + * @param string $header + * @param bool $replace + * + * @return void + */ + public function sendHeader(string $header, bool $replace = true): void + { + header($header, $replace); + } + + /** + * Set cookie. + * + * @param string $name + * @param string $value + * @param array $options + * + * @return void + */ + public function setCookie(string $name, string $value, array $options = []): void + { + setcookie($name, $value, $options); + } } \ No newline at end of file diff --git a/tests/Extension/AssetRuntimeExtensionTest.php b/tests/Extension/AssetRuntimeExtensionTest.php index 32d6525..530e035 100644 --- a/tests/Extension/AssetRuntimeExtensionTest.php +++ b/tests/Extension/AssetRuntimeExtensionTest.php @@ -87,10 +87,21 @@ public function testEntryPoints_withOptions() $extensionRuntime = new AssetRuntimeExtension($this->assets); $this->assertEquals( - '' . PHP_EOL . - '' . PHP_EOL . - '' . PHP_EOL, - $extensionRuntime->entryPoints('website', options: ['async' => true, 'defer' => true]) + '' . PHP_EOL . + '' . PHP_EOL . + '' . PHP_EOL, + $extensionRuntime->entryPoints('website', options: [ + 'async' => true, + 'defer' => true, + 'attr' => 'fake', + 'attr2' => null, + 'data' => [ + 'first' => 'value1', + 'second' => 'value2', + 'third' => true, + 'none' => null, + ] + ]) ); } @@ -137,4 +148,89 @@ public function testEntryPointsList_notFound() $this->assertEquals([], $extensionRuntime->entryPointsList('fake')); } + + public function providesPreload() + { + return [ + [ + 'link' => 'https://getberlioz.com/fake', + 'parameters' => [ + 'crossorigin' => false, + ], + 'expectedHeader' => 'Link: ; rel=preload', + 'expectedCookie' => [ + 'h2pushes[d51704684c8d3f1febc7c281cc2c8f26]', + '1', + [ + 'expires' => 0, + 'path' => '/', + 'domain' => '', + 'secure' => true, + 'httponly' => true, + 'samesite' => 'Strict', + ] + ], + ], + [ + 'link' => 'https://getberlioz.com/fake', + 'parameters' => [ + 'crossorigin' => true, + ], + 'expectedHeader' => 'Link: ; rel=preload; crossorigin', + 'expectedCookie' => [ + 'h2pushes[d51704684c8d3f1febc7c281cc2c8f26]', + '1', + [ + 'expires' => 0, + 'path' => '/', + 'domain' => '', + 'secure' => true, + 'httponly' => true, + 'samesite' => 'Strict', + ] + ], + ], + [ + 'link' => 'https://getberlioz.com/fake', + 'parameters' => [ + 'nopush' => true, + 'crossorigin' => true, + ], + 'expectedHeader' => 'Link: ; rel=preload; nopush; crossorigin', + 'expectedCookie' => null, + ] + ]; + } + + /** + * @dataProvider providesPreload + */ + public function testPreload(string $link, array $parameters, ?string $expectedHeader, ?array $expectedCookie) + { + $headerArguments = $cookieArguments = null; + + $assetRuntimeMock = $this->createPartialMock( + AssetRuntimeExtension::class, + ['isHeadersSent', 'sendHeader', 'setCookie'] + ); + $assetRuntimeMock + ->method('isHeadersSent') + ->willReturnCallback(fn() => false); + $assetRuntimeMock + ->method('sendHeader') + ->willReturnCallback(function ($header) use (&$headerArguments) { + $headerArguments = $header; + }); + $assetRuntimeMock + ->method('setCookie') + ->willReturnCallback(function (...$args) use (&$cookieArguments) { + $cookieArguments = $args; + }); + + $result = $assetRuntimeMock->preload($link, $parameters); + + $this->assertEquals($link, $result); + $this->assertEquals($expectedHeader, $headerArguments); + $this->assertEquals($expectedCookie, $cookieArguments); + } }