8000 [Asset] Add support for preloading with links and HTTP/2 push by dunglas · Pull Request #21478 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[Asset] Add support for preloading with links and HTTP/2 push #21478

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 16 commits into from
Closed
Prev Previous commit
Next Next commit
Preload is now a standalone Twig function
  • Loading branch information
dunglas committed Feb 7, 2017
commit 639d7d10f8d743c693394f6e1e0853b2de0f0507
43 changes: 24 additions & 19 deletions src/Symfony/Bridge/Twig/Extension/AssetExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Bridge\Twig\Extension;

use Symfony\Component\Asset\Packages;
use Symfony\Component\Asset\Preload\PreloadManagerInterface;

/**
* Twig extension for the Symfony Asset component.
Expand All @@ -21,10 +22,12 @@
class AssetExtension extends \Twig_Extension
{
private $packages;
private $preloadManager;

public function __construct(Packages $packages)
public function __construct(Packages $packages, PreloadManagerInterface $preloadManager = null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default PreloadManagerInterface implementation is simple and does not have dependencies, so I suppose it does not make sense to have a proper runtime.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I don't get what you mean.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we're just missing some doc about twig runtimes for extensions? :)

{
$this->packages = $packages;
$this->preloadManager = $preloadManager;
}

/**
Expand All @@ -34,8 +37,8 @@ public function getFunctions()
{
return array(
new \Twig_SimpleFunction('asset', array($this, 'getAssetUrl')),
new \Twig_SimpleFunction('preloaded_asset', array($this, 'getAndPreloadAssetUrl')),
new \Twig_SimpleFunction('asset_version', array($this, 'getAssetVersion')),
new \Twig_SimpleFunction('preload', array($this, 'preload')),
);
}

Expand All @@ -56,34 +59,36 @@ public function getAssetUrl($path, $packageName = null)
}

/**
* Returns the public url/path of an asset and preloads it.
*
* If the package used to generate the path is an instance of
* UrlPackage, you will always get a URL and not a path.
* Returns the version of 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
* @param string|null $packageName The name of the asset package to use
* @param string $path A public path
* @param string $packageName The name of the asset package to use
*
* @return string The public path of the asset
* @return string The asset version
*/
public function getAndPreloadAssetUrl($path, $as = '', $nopush = false, $packageName = null)
public function getAssetVersion($path, $packageName = null)
{
return $this->packages->getAndPreloadUrl($path, $as, $nopush, $packageName);
return $this->packages->getVersion($path, $packageName);
}

/**
* Returns the version of an asset.
* Preloads an asset.
*
* @param string $path A public path
* @param string $packageName The name of the asset package to use
* @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 asset version
* @return string The path of the asset
*/
public function getAssetVersion($path, $packageName = null)
public function preload($path, $as = '', $nopush = false)
{
return $this->packages->getVersion($path, $packageName);
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;
}

/**
Expand Down
15 changes: 11 additions & 4 deletions src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@
namespace Symfony\Bridge\Twig\Tests\Extension;

use Symfony\Bridge\Twig\Extension\AssetExtension;
use Symfony\Component\Asset\Package;
use Symfony\Component\Asset\Packages;
use Symfony\Component\Asset\Preload\HttpFoundationPreloadManager;
use Symfony\Component\Asset\VersionStrategy\EmptyVersionStrategy;

/**
* @author Kévin Dunglas <dunglas@gmail.com>
Expand All @@ -25,9 +23,18 @@ class AssetExtensionTest extends \PHPUnit_Framework_TestCase
public function testGetAndPreloadAssetUrl()
{
$preloadManager = new HttpFoundationPreloadManager();
$extension = new AssetExtension(new Packages(new Package(new EmptyVersionStrategy(), null, $preloadManager)));
$extension = new AssetExtension(new Packages(), $preloadManager);

$this->assertEquals('/foo.css', $extension->getAndPreloadAssetUrl('/foo.css', 'style', true));
$this->assertEquals('/foo.css', $extension->preload('/foo.css', 'style', true));
$this->assertEquals(array('/foo.css' => array('as' => 'style', 'nopush' => true)), $preloadManager->getResources());
}

/**
* @expectedException \RuntimeException
*/
public function testNoConfiguredPreloadManager()
{
$extension = new AssetExtension(new Packages());
$extension->preload('/foo.css');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -772,10 +772,7 @@ private function registerAssetsConfiguration(array $config, ContainerBuilder $co

$preloadListener = $container->register('asset.preload_listener', PreloadListener::class);
$preloadListener->addArgument(new Reference('assets.preload_manager'));
$preloadListener->addTag('kernel.event_listener', array(
'event' => 'kernel.response',
'method' => 'onKernelResponse',
));
$preloadListener->addTag('kernel.event_subscriber');
}

$defaultPackage = $this->createPackageDefinition($config['base_path'], $config['base_urls'], $defaultVersion);
Expand Down Expand Up @@ -819,10 +816,6 @@ private function createPackageDefinition($basePath, array $baseUrls, Reference $
->replaceArgument(1, $version)
;

if (class_exists(HttpFoundationPreloadManager::class)) {
$package->addArgument(new Reference('assets.preload_manager'));
}

return $package;
}

Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@

<service id="twig.extension.assets" class="Symfony\Bridge\Twig\Extension\AssetExtension" public="false">
<argument type="service" id="assets.packages" />
<argument type="service" id="assets.preload_manager" on-invalid="ignore" />
</service>

<service id="twig.extension.code" class="Symfony\Bridge\Twig\Extension\CodeExtension" public="false">
Expand Down
15 changes: 14 additions & 1 deletion src/Symfony/Component/Asset/EventListener/PreloadListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@
namespace Symfony\Component\Asset\EventListener;

use Symfony\Component\Asset\Preload\HttpFoundationPreloadManager;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

/**
* Adds preload's Link HTTP headers to the response.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class PreloadListener
class PreloadListener implements EventSubscriberInterface
{
private $preloadManager;

Expand All @@ -31,5 +33,16 @@ public function __construct(HttpFoundationPreloadManager $preloadManager)
public function onKernelResponse(FilterResponseEvent $event)
{
$this->preloadManager->setLinkHeader($event->getResponse());

// Free memory
$this->preloadManager->setResources(array());
}

/**
* {@inheritdoc}
*/
public static function getSubscribedEvents()
{
return array(KernelEvents::RESPONSE => 'onKernelResponse');
}
}
22 changes: 2 additions & 20 deletions src/Symfony/Component/Asset/Package.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,16 @@
*
* @author Kris Wallsmith <kris@symfony.com>
* @author Fabien Potencier <fabien@symfony.com>
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class Package implements PreloadedPackageInterface
class Package implements PackageInterface
{
private $versionStrategy;
private $context;
private $preloadManager;

public function __construct(VersionStrategyInterface $versionStrategy, ContextInterface $context = null, PreloadManagerInterface $preloadManager = null)
public function __construct(VersionStrategyInterface $versionStrategy, ContextInterface $context = null)
{
$this->versionStrategy = $versionStrategy;
$this->context = $context ?: new NullContext();
$this->preloadManager = $preloadManager;
}

/**
Expand All @@ -57,21 +54,6 @@ public function getUrl($path)
return $this->versionStrategy->applyVersion($path);
}

/**
* {@inheritdoc}
*/
public function getAndPreloadUrl($path, $as = '', $nopush = false)
{
if (null === $this->preloadManager) {
throw new LogicException('There is no preload manager, configure one first.');
}

$url = $this->getUrl($path);
$this->preloadManager->addResource($url, $as, $nopush);

return $url;
}

/**
* @return ContextInterface
*/
Expand Down
24 changes: 0 additions & 24 deletions src/Symfony/Component/Asset/Packages.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,28 +113,4 @@ public function getUrl($path, $packageName = null)
{
return $this->getPackage($packageName)->getUrl($path);
}

/**
* Returns the public path and preloads it.
*
* Absolute paths (i.e. http://...) are returned unmodified.
*
* @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
* @param string $packageName The name of the asset package to use
*
* @return string A public path which takes into account the base path and URL path
*
* @throws InvalidArgumentException If the requested package doesn't support preloading
*/
public function getAndPreloadUrl($path, $as = '', $nopush = false, $packageName = null)
{
$package = $this->getPackage($packageName);
if (!$package instanceof PreloadedPackageInterface) {
throw new InvalidArgumentException(sprintf('The "%s" package doesn\'t support preloading.', $packageName ?: 'default'));
}

return $package->getAndPreloadUrl($path, $as, $nopush);
}
}
31 changes: 0 additions & 31 deletions src/Symfony/Component/Asset/PreloadedPackageInterface.php

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@

use Symfony\Component\Asset\EventListener\PreloadListener;
use Symfony\Component\Asset\Preload\HttpFoundationPreloadManager;
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>
Expand All @@ -26,13 +28,21 @@ public function testOnKernelResponse()
$manager = new HttpFoundationPreloadManager();
$manager->addResource('/foo');

$listener = new PreloadListener($manager);
$subscriber = new PreloadListener($manager);
$response = new Response();

$event = $this->getMockBuilder(FilterResponseEvent::class)->disableOriginalConstructor()->getMock();
$event->method('getResponse')->willReturn($response);

$listener->onKernelResponse($event);
$subscriber->onKernelResponse($event);

$this->assertInstanceOf(EventSubscriberInterface::class, $subscriber);
$this->assertEquals('</foo>; rel=preload', $response->headers->get('Link'));
$this->assertEmpty($manager->getResources());
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add a test where the response already has another Link header previously


public function testSubscribedEvents()
{
$this->assertEquals(array(KernelEvents::RESPONSE => 'onKernelResponse'), PreloadListener::getSubscribedEvents());
}
}
17 changes: 0 additions & 17 deletions src/Symfony/Component/Asset/Tests/PackageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,21 +52,4 @@ public function testGetVersion()
$package = new Package(new StaticVersionStrategy('v1'));
$this->assertEquals('v1', $package->getVersion('/foo'));
}

public function testGetAndPreloadUrl()
{
$preloadManager = $this->getMockBuilder(PreloadManagerInterface::class)->getMock();
$preloadManager
->expects($this->exactly(2))
->method('addResource')
->withConsecutive(
179B array($this->equalTo('/foo'), $this->equalTo(''), $this->equalTo(false)),
array($this->equalTo('/bar'), $this->equalTo('script'), $this->equalTo(true))
)
;

$package = new Package(new EmptyVersionStrategy(), null, $preloadManager);
$this->assertEquals('/foo', $package->getAndPreloadUrl('/foo'));
$this->assertEquals('/bar', $package->getAndPreloadUrl('/bar', 'script', true));
}
}
30 changes: 0 additions & 30 deletions src/Symfony/Component/Asset/Tests/PackagesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,27 +56,6 @@ public function testGetUrl()
$this->assertEquals('/foo?a', $packages->getUrl('/foo', 'a'));
}

public function testGetAndPreloadUrl()
{
$preloadManager = $this->getMockBuilder(PreloadManagerInterface::class)->getMock();
$preloadManager
->expects($this->exactly(2))
->method('addResource')
->withConsecutive(
array($this->equalTo('/foo?default'), $this->equalTo(''), $this->equalTo(false)),
array($this->equalTo('/foo?a'), $this->equalTo('script'), $this->equalTo(true))
)
;

$packages = new Packages(
new Package(new StaticVersionStrategy('default'), null, $preloadManager),
array('a' => new Package(new StaticVersionStrategy('a'), null, $preloadManager))
);

$this->assertEquals('/foo?default', $packages->getAndPreloadUrl('/foo'));
$this->assertEquals('/foo?a', $packages->getAndPreloadUrl('/foo', 'script', true, 'a'));
}

/**
* @expectedException \Symfony\Component\Asset\Exception\LogicException
*/
Expand All @@ -94,13 +73,4 @@ public function testUndefinedPackage()
$packages = new Packages();
$packages->getPackage('a');
}

/**
* @expectedException \Symfony\Component\Asset\Exception\InvalidArgumentException
*/
public function testDoesNotSupportPreloading()
{
$packages = new Packages($this->getMockBuilder(PackageInterface::class)->getMock());
$packages->getAndPreloadUrl('/foo');
}
}
0