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);
+ }
}