[Asset] Add support for preloading with links and HTTP/2 push
This commit is contained in:
parent
554b1a748f
commit
7bab21700d
@ -12,6 +12,7 @@
|
|||||||
namespace Symfony\Bridge\Twig\Extension;
|
namespace Symfony\Bridge\Twig\Extension;
|
||||||
|
|
||||||
use Symfony\Component\Asset\Packages;
|
use Symfony\Component\Asset\Packages;
|
||||||
|
use Symfony\Component\Asset\Preload\PreloadManagerInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Twig extension for the Symfony Asset component.
|
* Twig extension for the Symfony Asset component.
|
||||||
@ -21,10 +22,12 @@ use Symfony\Component\Asset\Packages;
|
|||||||
class AssetExtension extends \Twig_Extension
|
class AssetExtension extends \Twig_Extension
|
||||||
{
|
{
|
||||||
private $packages;
|
private $packages;
|
||||||
|
private $preloadManager;
|
||||||
|
|
||||||
public function __construct(Packages $packages)
|
public function __construct(Packages $packages, PreloadManagerInterface $preloadManager = null)
|
||||||
{
|
{
|
||||||
$this->packages = $packages;
|
$this->packages = $packages;
|
||||||
|
$this->preloadManager = $preloadManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -35,6 +38,7 @@ class AssetExtension extends \Twig_Extension
|
|||||||
return array(
|
return array(
|
||||||
new \Twig_SimpleFunction('asset', array($this, 'getAssetUrl')),
|
new \Twig_SimpleFunction('asset', array($this, 'getAssetUrl')),
|
||||||
new \Twig_SimpleFunction('asset_version', array($this, 'getAssetVersion')),
|
new \Twig_SimpleFunction('asset_version', array($this, 'getAssetVersion')),
|
||||||
|
new \Twig_SimpleFunction('preload', array($this, 'preload')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,6 +71,26 @@ class AssetExtension extends \Twig_Extension
|
|||||||
return $this->packages->getVersion($path, $packageName);
|
return $this->packages->getVersion($path, $packageName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preloads an asset.
|
||||||
|
*
|
||||||
|
* @param string $path A public path
|
||||||
|
* @param string $as A valid destination according to https://fetch.spec.whatwg.org/#concept-request-destination
|
||||||
|
* @param bool $nopush If this asset should not be pushed over HTTP/2
|
||||||
|
*
|
||||||
|
* @return string The path of the asset
|
||||||
|
*/
|
||||||
|
public function preload($path, $as = '', $nopush = false)
|
||||||
|
{
|
||||||
|
if (null === $this->preloadManager) {
|
||||||
|
throw new \RuntimeException('A preload manager must be configured to use the "preload" function.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->preloadManager->addResource($path, $as, $nopush);
|
||||||
|
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the name of the extension.
|
* Returns the name of the extension.
|
||||||
*
|
*
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Bridge\Twig\Tests\Extension;
|
||||||
|
|
||||||
|
use Symfony\Bridge\Twig\Extension\AssetExtension;
|
||||||
|
use Symfony\Component\Asset\Packages;
|
||||||
|
use Symfony\Component\Asset\Preload\PreloadManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Kévin Dunglas <dunglas@gmail.com>
|
||||||
|
*/
|
||||||
|
class AssetExtensionTest extends \PHPUnit_Framework_TestCase
|
||||||
|
{
|
||||||
|
public function testGetAndPreloadAssetUrl()
|
||||||
|
{
|
||||||
|
if (!class_exists(PreloadManager::class)) {
|
||||||
|
$this->markTestSkipped('Requires Asset 3.3+.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$preloadManager = new PreloadManager();
|
||||||
|
$extension = new AssetExtension(new Packages(), $preloadManager);
|
||||||
|
|
||||||
|
$this->assertEquals('/foo.css', $extension->preload('/foo.css', 'style', true));
|
||||||
|
$this->assertEquals('</foo.css>; rel=preload; as=style; nopush', $preloadManager->buildLinkValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException \RuntimeException
|
||||||
|
*/
|
||||||
|
public function testNoConfiguredPreloadManager()
|
||||||
|
{
|
||||||
|
$extension = new AssetExtension(new Packages());
|
||||||
|
$extension->preload('/foo.css');
|
||||||
|
}
|
||||||
|
}
|
@ -38,5 +38,13 @@
|
|||||||
|
|
||||||
<service id="assets.empty_version_strategy" class="Symfony\Component\Asset\VersionStrategy\EmptyVersionStrategy" public="false" />
|
<service id="assets.empty_version_strategy" class="Symfony\Component\Asset\VersionStrategy\EmptyVersionStrategy" public="false" />
|
||||||
|
|
||||||
|
<service id="assets.preload_manager" class="Symfony\Component\Asset\Preload\PreloadManager" public="false" />
|
||||||
|
|
||||||
|
<service id="asset.preload_listener" class="Symfony\Component\Asset\EventListener\PreloadListener">
|
||||||
|
<argument type="service" id="assets.preload_manager" />
|
||||||
|
|
||||||
|
<tag name="kernel.event_subscriber" />
|
||||||
|
</service>
|
||||||
|
|
||||||
</services>
|
</services>
|
||||||
</container>
|
</container>
|
||||||
|
@ -375,6 +375,12 @@ abstract class FrameworkExtensionTest extends TestCase
|
|||||||
$this->assertEquals('assets.custom_version_strategy', (string) $defaultPackage->getArgument(1));
|
$this->assertEquals('assets.custom_version_strategy', (string) $defaultPackage->getArgument(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testAssetHasPreloadListener()
|
||||||
|
{
|
||||||
|
$container = $this->createContainerFromFile('assets');
|
||||||
|
$this->assertTrue($container->hasDefinition('asset.preload_listener'));
|
||||||
|
}
|
||||||
|
|
||||||
public function testTranslator()
|
public function testTranslator()
|
||||||
{
|
{
|
||||||
$container = $this->createContainerFromFile('full');
|
$container = $this->createContainerFromFile('full');
|
||||||
|
@ -60,6 +60,7 @@
|
|||||||
"conflict": {
|
"conflict": {
|
||||||
"phpdocumentor/reflection-docblock": "<3.0",
|
"phpdocumentor/reflection-docblock": "<3.0",
|
||||||
"phpdocumentor/type-resolver": "<0.2.0",
|
"phpdocumentor/type-resolver": "<0.2.0",
|
||||||
|
"symfony/asset": "<3.3",
|
||||||
"symfony/console": "<3.3"
|
"symfony/console": "<3.3"
|
||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
|
@ -70,6 +70,7 @@
|
|||||||
|
|
||||||
<service id="twig.extension.assets" class="Symfony\Bridge\Twig\Extension\AssetExtension" public="false">
|
<service id="twig.extension.assets" class="Symfony\Bridge\Twig\Extension\AssetExtension" public="false">
|
||||||
<argument type="service" id="assets.packages" />
|
<argument type="service" id="assets.packages" />
|
||||||
|
<argument type="service" id="assets.preload_manager" on-invalid="ignore" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service id="twig.extension.code" class="Symfony\Bridge\Twig\Extension\CodeExtension" public="false">
|
<service id="twig.extension.code" class="Symfony\Bridge\Twig\Extension\CodeExtension" public="false">
|
||||||
|
@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Asset\EventListener;
|
||||||
|
|
||||||
|
use Symfony\Component\Asset\Preload\PreloadManager;
|
||||||
|
use Symfony\Component\Asset\Preload\PreloadManagerInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
|
||||||
|
use Symfony\Component\HttpKernel\KernelEvents;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the preload Link HTTP header to the response.
|
||||||
|
*
|
||||||
|
* @author Kévin Dunglas <dunglas@gmail.com>
|
||||||
|
*/
|
||||||
|
class PreloadListener implements EventSubscriberInterface
|
||||||
|
{
|
||||||
|
private $preloadManager;
|
||||||
|
|
||||||
|
public function __construct(PreloadManagerInterface $preloadManager)
|
||||||
|
{
|
||||||
|
$this->preloadManager = $preloadManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onKernelResponse(FilterResponseEvent $event)
|
||||||
|
{
|
||||||
|
if (!$event->isMasterRequest()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value = $this->preloadManager->buildLinkValue()) {
|
||||||
|
$event->getResponse()->headers->set('Link', $value, false);
|
||||||
|
|
||||||
|
// Free memory
|
||||||
|
$this->preloadManager->clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public static function getSubscribedEvents()
|
||||||
|
{
|
||||||
|
return array(KernelEvents::RESPONSE => 'onKernelResponse');
|
||||||
|
}
|
||||||
|
}
|
58
src/Symfony/Component/Asset/Preload/PreloadManager.php
Normal file
58
src/Symfony/Component/Asset/Preload/PreloadManager.php
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Asset\Preload;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages preload HTTP headers.
|
||||||
|
*
|
||||||
|
* @author Kévin Dunglas <dunglas@gmail.com>
|
||||||
|
*/
|
||||||
|
class PreloadManager implements PreloadManagerInterface
|
||||||
|
{
|
||||||
|
private $resources = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function addResource($uri, $as = '', $nopush = false)
|
||||||
|
{
|
||||||
|
$this->resources[$uri] = array('as' => $as, 'nopush' => $nopush);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function clear()
|
||||||
|
{
|
||||||
|
$this->resources = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function buildLinkValue()
|
||||||
|
{
|
||||||
|
if (!$this->resources) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = array();
|
||||||
|
foreach ($this->resources as $uri => $options) {
|
||||||
|
$as = '' === $options['as'] ? '' : sprintf('; as=%s', $options['as']);
|
||||||
|
$nopush = $options['nopush'] ? '; nopush' : '';
|
||||||
|
|
||||||
|
$parts[] = sprintf('<%s>; rel=preload%s%s', $uri, $as, $nopush);
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(',', $parts);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Asset\Preload;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages resources to preload according to the W3C "Preload" specification.
|
||||||
|
*
|
||||||
|
* @see https://www.w3.org/TR/preload/
|
||||||
|
*
|
||||||
|
* @author Kévin Dunglas <dunglas@gmail.com>
|
||||||
|
*/
|
||||||
|
interface PreloadManagerInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Adds an element to the list of resources to preload.
|
||||||
|
*
|
||||||
|
* @param string $uri The resource URI
|
||||||
|
* @param string $as A valid destination according to https://fetch.spec.whatwg.org/#concept-request-destination
|
||||||
|
* @param bool $nopush If this asset should not be pushed over HTTP/2
|
||||||
|
*/
|
||||||
|
public function addResource($uri, $as = '', $nopush = false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the list of resources.
|
||||||
|
*/
|
||||||
|
public function clear();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the value of the preload Link HTTP header.
|
||||||
|
*
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function buildLinkValue();
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Asset\Tests\EventListener;
|
||||||
|
|
||||||
|
use Symfony\Component\Asset\EventListener\PreloadListener;
|
||||||
|
use Symfony\Component\Asset\Preload\PreloadManager;
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
|
||||||
|
use Symfony\Component\HttpKernel\KernelEvents;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Kévin Dunglas <dunglas@gmail.com>
|
||||||
|
*/
|
||||||
|
class PreloadListenerTest extends \PHPUnit_Framework_TestCase
|
||||||
|
{
|
||||||
|
public function testOnKernelResponse()
|
||||||
|
{
|
||||||
|
$manager = new PreloadManager();
|
||||||
|
$manager->addResource('/foo');
|
||||||
|
|
||||||
|
$subscriber = new PreloadListener($manager);
|
||||||
|
$response = new Response('', 200, array('Link' => '<https://demo.api-platform.com/docs.jsonld>; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"'));
|
||||||
|
|
||||||
|
$event = $this->getMockBuilder(FilterResponseEvent::class)->disableOriginalConstructor()->getMock();
|
||||||
|
$event->method('isMasterRequest')->willReturn(true);
|
||||||
|
$event->method('getResponse')->willReturn($response);
|
||||||
|
|
||||||
|
$subscriber->onKernelResponse($event);
|
||||||
|
|
||||||
|
$this->assertInstanceOf(EventSubscriberInterface::class, $subscriber);
|
||||||
|
|
||||||
|
$expected = array(
|
||||||
|
'<https://demo.api-platform.com/docs.jsonld>; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"',
|
||||||
|
'</foo>; rel=preload',
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals($expected, $response->headers->get('Link', null, false));
|
||||||
|
$this->assertNull($manager->buildLinkValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSubscribedEvents()
|
||||||
|
{
|
||||||
|
$this->assertEquals(array(KernelEvents::RESPONSE => 'onKernelResponse'), PreloadListener::getSubscribedEvents());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Asset\Preload;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Kévin Dunglas <dunglas@gmail.com>
|
||||||
|
*/
|
||||||
|
class PreloadManagerTest extends \PHPUnit_Framework_TestCase
|
||||||
|
{
|
||||||
|
public function testManageResources()
|
||||||
|
{
|
||||||
|
$manager = new PreloadManager();
|
||||||
|
$this->assertInstanceOf(PreloadManagerInterface::class, $manager);
|
||||||
|
|
||||||
|
$manager->addResource('/foo/bar.js', 'script', false);
|
||||||
|
$manager->addResource('/foo/baz.css');
|
||||||
|
$manager->addResource('/foo/bat.png', 'image', true);
|
||||||
|
|
||||||
|
$this->assertEquals('</foo/bar.js>; rel=preload; as=script,</foo/baz.css>; rel=preload,</foo/bat.png>; rel=preload; as=image; nopush', $manager->buildLinkValue());
|
||||||
|
}
|
||||||
|
}
|
@ -22,7 +22,8 @@
|
|||||||
"symfony/http-foundation": ""
|
"symfony/http-foundation": ""
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"symfony/http-foundation": "~2.8|~3.0"
|
"symfony/http-foundation": "~2.8|~3.0",
|
||||||
|
"symfony/http-kernel": "~2.8|~3.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": { "Symfony\\Component\\Asset\\": "" },
|
"psr-4": { "Symfony\\Component\\Asset\\": "" },
|
||||||
|
Reference in New Issue
Block a user