From 64bd530dff180c344d62e991147fdf30c57c4397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 31 Jan 2017 16:53:28 +0100 Subject: [PATCH 01/16] [Asset] Add suport for preloading with links and HTTP/2 push --- .../Bridge/Twig/Extension/AssetExtension.php | 18 +++++ src/Symfony/Bridge/Twig/composer.json | 2 +- .../FrameworkExtension.php | 36 ++++++---- .../Asset/EventListener/PreloadListener.php | 35 ++++++++++ src/Symfony/Component/Asset/Package.php | 24 ++++++- src/Symfony/Component/Asset/Packages.php | 21 ++++++ .../Preload/HttpFoundationPreloadManager.php | 66 +++++++++++++++++++ .../Asset/Preload/PreloadManagerInterface.php | 43 ++++++++++++ .../Asset/PreloadedPackageInterface.php | 30 +++++++++ 9 files changed, 259 insertions(+), 16 deletions(-) create mode 100644 src/Symfony/Component/Asset/EventListener/PreloadListener.php create mode 100644 src/Symfony/Component/Asset/Preload/HttpFoundationPreloadManager.php create mode 100644 src/Symfony/Component/Asset/Preload/PreloadManagerInterface.php create mode 100644 src/Symfony/Component/Asset/PreloadedPackageInterface.php diff --git a/src/Symfony/Bridge/Twig/Extension/AssetExtension.php b/src/Symfony/Bridge/Twig/Extension/AssetExtension.php index f599a9eb5c9b6..d5aba137131a6 100644 --- a/src/Symfony/Bridge/Twig/Extension/AssetExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/AssetExtension.php @@ -34,6 +34,7 @@ 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')), ); } @@ -54,6 +55,23 @@ public function getAssetUrl($path, $packageName = null) return $this->packages->getUrl($path, $packageName); } + /** + * 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. + * + * @param string $path A public path + * @param string $as A valid destination according to https://fetch.spec.whatwg.org/#concept-request-destination + * @param string $packageName The name of the asset package to use + * + * @return string The public path of the asset + */ + public function getAndPreloadAssetUrl($path, $as = '', $packageName) + { + return $this->packages->getAndPreloadUrl($path, $as, $packageName); + } + /** * Returns the version of an asset. * diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index 845649cb251f2..e540fec40b543 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -20,7 +20,7 @@ "twig/twig": "~1.28|~2.0" }, "require-dev": { - "symfony/asset": "~2.8|~3.0", + "symfony/asset": "~3.3", "symfony/finder": "~2.8|~3.0", "symfony/form": "~3.1.9|^3.2.2", "symfony/http-kernel": "~3.2", diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 88ac0c0655e9d..60166802e4506 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -13,6 +13,8 @@ use Doctrine\Common\Annotations\Reader; use Symfony\Bridge\Monolog\Processor\DebugProcessor; +use Symfony\Component\Asset\EventListener\PreloadListener; +use Symfony\Component\Asset\Preload\HttpFoundationPreloadManager; use Symfony\Component\Cache\Adapter\AdapterInterface; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\Alias; @@ -764,6 +766,18 @@ private function registerAssetsConfiguration(array $config, ContainerBuilder $co $defaultVersion = $this->createVersion($container, $config['version'], $config['version_format'], '_default'); } + if (class_exists(HttpFoundationPreloadManager::class)) { + $preloadManagerDefinition = $container->register('assets.preload_manager', HttpFoundationPreloadManager::class); + $preloadManagerDefinition->setPublic(false); + + $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' + )); + } + $defaultPackage = $this->createPackageDefinition($config['base_path'], $config['base_urls'], $defaultVersion); $container->setDefinition('assets._default_package', $defaultPackage); @@ -797,23 +811,19 @@ private function createPackageDefinition($basePath, array $baseUrls, Reference $ throw new \LogicException('An asset package cannot have base URLs and base paths.'); } - if (!$baseUrls) { - $package = new ChildDefinition('assets.path_package'); + $package = new ChildDefinition($baseUrls ? 'assets.url_package' : 'assets.path_package'); - return $package - ->setPublic(false) - ->replaceArgument(0, $basePath) - ->replaceArgument(1, $version) - ; - } - - $package = new ChildDefinition('assets.url_package'); - - return $package + $package ->setPublic(false) - ->replaceArgument(0, $baseUrls) + ->replaceArgument(0, $baseUrls ?: $basePath) ->replaceArgument(1, $version) ; + + if (class_exists(HttpFoundationPreloadManager::class)) { + $package->addArgument(new Reference('assets.preload_manager')); + } + + return $package; } private function createVersion(ContainerBuilder $container, $version, $format, $name) diff --git a/src/Symfony/Component/Asset/EventListener/PreloadListener.php b/src/Symfony/Component/Asset/EventListener/PreloadListener.php new file mode 100644 index 0000000000000..57c77448b6301 --- /dev/null +++ b/src/Symfony/Component/Asset/EventListener/PreloadListener.php @@ -0,0 +1,35 @@ + + * + * 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\HttpFoundationPreloadManager; +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; + +/** + * Adds preload's Link HTTP headers to the response. + * + * @author Kévin Dunglas + */ +class PreloadListener +{ + private $preloadManager; + + public function __construct(HttpFoundationPreloadManager $preloadManager) + { + $this->preloadManager = $preloadManager; + } + + public function onKernelResponse(FilterResponseEvent $event) + { + $this->preloadManager->setLinkHeader($event->getResponse()); + } +} diff --git a/src/Symfony/Component/Asset/Package.php b/src/Symfony/Component/Asset/Package.php index 77b1c934eb172..9b11510d49898 100644 --- a/src/Symfony/Component/Asset/Package.php +++ b/src/Symfony/Component/Asset/Package.php @@ -13,6 +13,8 @@ use Symfony\Component\Asset\Context\ContextInterface; use Symfony\Component\Asset\Context\NullContext; +use Symfony\Component\Asset\Exception\LogicException; +use Symfony\Component\Asset\Preload\PreloadManagerInterface; use Symfony\Component\Asset\VersionStrategy\VersionStrategyInterface; /** @@ -20,16 +22,19 @@ * * @author Kris Wallsmith * @author Fabien Potencier + * @author Kévin Dunglas */ -class Package implements PackageInterface +class Package implements PreloadedPackageInterface { private $versionStrategy; private $context; + private $preloadManager; - public function __construct(VersionStrategyInterface $versionStrategy, ContextInterface $context = null) + public function __construct(VersionStrategyInterface $versionStrategy, ContextInterface $context = null, PreloadManagerInterface $preloadManager = null) { $this->versionStrategy = $versionStrategy; $this->context = $context ?: new NullContext(); + $this->preloadManager = $preloadManager; } /** @@ -52,6 +57,21 @@ public function getUrl($path) return $this->versionStrategy->applyVersion($path); } + /** + * {@inheritdoc} + */ + public function getAndPreloadUrl($path, $as = '') + { + if (null === $this->preloadManager) { + throw new LogicException('There is no preload manager, configure one first.'); + } + + $url = $this->getUrl($path); + $this->preloadManager->addResource($url, $as); + + return $url; + } + /** * @return ContextInterface */ diff --git a/src/Symfony/Component/Asset/Packages.php b/src/Symfony/Component/Asset/Packages.php index 0a8c96354967c..3be7c04bae8dc 100644 --- a/src/Symfony/Component/Asset/Packages.php +++ b/src/Symfony/Component/Asset/Packages.php @@ -113,4 +113,25 @@ 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 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 + */ + public function getAndPreloadUrl($path, $as = '', $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); + } } diff --git a/src/Symfony/Component/Asset/Preload/HttpFoundationPreloadManager.php b/src/Symfony/Component/Asset/Preload/HttpFoundationPreloadManager.php new file mode 100644 index 0000000000000..08de3d1f3339a --- /dev/null +++ b/src/Symfony/Component/Asset/Preload/HttpFoundationPreloadManager.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Asset\Preload; +use Symfony\Component\HttpFoundation\Response; + +/** + * Preload manager for the HttpFoundation component. + * + * @author Kévin Dunglas + */ +class HttpFoundationPreloadManager implements PreloadManagerInterface +{ + private $resources = array(); + + /** + * {@inheritdoc} + */ + public function addResource($uri, $as) + { + $this->resources[$uri] = $as; + } + + /** + * {@inheritdoc} + */ + public function getResources() + { + return $this->resources; + } + + /** + * {@inheritdoc} + */ + public function setResources(array $resources) + { + $this->resources = $resources; + } + + /** + * Sets preload Link HTTP header. + * + * @param Response $response + */ + public function setLinkHeader(Response $response) + { + if (!$this->resources) { + return; + } + + $parts = array(); + foreach ($this->resources as $uri => $as) { + $parts[] = "<$uri>; rel=preload; as=$as"; + } + + $response->headers->set('Link', implode(',', $parts)); + } +} diff --git a/src/Symfony/Component/Asset/Preload/PreloadManagerInterface.php b/src/Symfony/Component/Asset/Preload/PreloadManagerInterface.php new file mode 100644 index 0000000000000..7cd5a21eef5fd --- /dev/null +++ b/src/Symfony/Component/Asset/Preload/PreloadManagerInterface.php @@ -0,0 +1,43 @@ + + * + * 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 + */ +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 + */ + public function addResource($uri, $as); + + /** + * Gets the list of resources to preload. + * + * @return array + */ + public function getResources(); + + /** + * Replaces the list of resources. + * + * @param array $resources + */ + public function setResources(array $resources); +} diff --git a/src/Symfony/Component/Asset/PreloadedPackageInterface.php b/src/Symfony/Component/Asset/PreloadedPackageInterface.php new file mode 100644 index 0000000000000..856ade4798f7f --- /dev/null +++ b/src/Symfony/Component/Asset/PreloadedPackageInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Asset; + +/** + * Asset package with preloading support interface. + * + * @author Kévin Dunglas + */ +interface PreloadedPackageInterface extends PackageInterface +{ + /** + * Returns an absolute or root-relative public path and adds it to the URLs to preload. + * + * @param string $path A path + * @param string $as A valid destination according to https://fetch.spec.whatwg.org/#concept-request-destination + * + * @return string + */ + public function getAndPreloadUrl($path, $as = ''); +} From f920ca4751a37d3617664cb0fecbb0bc5fbca076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 31 Jan 2017 21:59:53 +0100 Subject: [PATCH 02/16] Add tests and nopush support --- .../Bridge/Twig/Extension/AssetExtension.php | 11 ++-- .../Tests/Extension/AssetExtensionTest.php | 33 ++++++++++ .../FrameworkExtension.php | 4 +- .../FrameworkExtensionTest.php | 2 + src/Symfony/Component/Asset/Package.php | 4 +- src/Symfony/Component/Asset/Packages.php | 7 ++- .../Preload/HttpFoundationPreloadManager.php | 33 ++++++++-- .../Asset/Preload/PreloadManagerInterface.php | 8 ++- .../Asset/PreloadedPackageInterface.php | 8 +-- .../EventListener/PreloadListenerTest.php | 38 ++++++++++++ .../Component/Asset/Tests/PackageTest.php | 18 ++++++ .../Component/Asset/Tests/PackagesTest.php | 32 ++++++++++ .../HttpFoundationPreloadManagerTest.php | 60 +++++++++++++++++++ 13 files changed, 236 insertions(+), 22 deletions(-) create mode 100644 src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php create mode 100644 src/Symfony/Component/Asset/Tests/EventListener/PreloadListenerTest.php create mode 100644 src/Symfony/Component/Asset/Tests/Preload/HttpFoundationPreloadManagerTest.php diff --git a/src/Symfony/Bridge/Twig/Extension/AssetExtension.php b/src/Symfony/Bridge/Twig/Extension/AssetExtension.php index d5aba137131a6..ffc2a2fd4800a 100644 --- a/src/Symfony/Bridge/Twig/Extension/AssetExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/AssetExtension.php @@ -61,15 +61,16 @@ public function getAssetUrl($path, $packageName = null) * If the package used to generate the path is an instance of * UrlPackage, you will always get a URL and not a path. * - * @param string $path A public path - * @param string $as A valid destination according to https://fetch.spec.whatwg.org/#concept-request-destination - * @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 + * @param string|null $packageName The name of the asset package to use * * @return string The public path of the asset */ - public function getAndPreloadAssetUrl($path, $as = '', $packageName) + public function getAndPreloadAssetUrl($path, $as = '', $nopush = false, $packageName = null) { - return $this->packages->getAndPreloadUrl($path, $as, $packageName); + return $this->packages->getAndPreloadUrl($path, $as, $nopush, $packageName); } /** diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php new file mode 100644 index 0000000000000..89ab0c83cb774 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php @@ -0,0 +1,33 @@ + + * + * 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\Package; +use Symfony\Component\Asset\Packages; +use Symfony\Component\Asset\Preload\HttpFoundationPreloadManager; +use Symfony\Component\Asset\VersionStrategy\EmptyVersionStrategy; + +/** + * @author Kévin Dunglas + */ +class AssetExtensionTest extends \PHPUnit_Framework_TestCase +{ + public function testGetAndPreloadAssetUrl() + { + $preloadManager = new HttpFoundationPreloadManager(); + $extension = new AssetExtension(new Packages(new Package(new EmptyVersionStrategy(), null, $preloadManager))); + + $this->assertEquals('/foo.css', $extension->getAndPreloadAssetUrl('/foo.css', 'style', true)); + $this->assertEquals(array('/foo.css' => array('as' => 'style', 'nopush' => true)), $preloadManager->getResources()); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 60166802e4506..ad73a65425ea0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -773,8 +773,8 @@ 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' + 'event' => 'kernel.response', + 'method' => 'onKernelResponse', )); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 0d67abeda1ed3..1121c476d7357 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -363,6 +363,8 @@ public function testAssets() $package = $container->getDefinition((string) $packages['bar_version_strategy']); $this->assertEquals('assets.custom_version_strategy', (string) $package->getArgument(1)); + + $this->assertTrue($container->hasDefinition('asset.preload_listener')); } public function testAssetsDefaultVersionStrategyAsService() diff --git a/src/Symfony/Component/Asset/Package.php b/src/Symfony/Component/Asset/Package.php index 9b11510d49898..419c62bd74ef7 100644 --- a/src/Symfony/Component/Asset/Package.php +++ b/src/Symfony/Component/Asset/Package.php @@ -60,14 +60,14 @@ public function getUrl($path) /** * {@inheritdoc} */ - public function getAndPreloadUrl($path, $as = '') + 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); + $this->preloadManager->addResource($url, $as, $nopush); return $url; } diff --git a/src/Symfony/Component/Asset/Packages.php b/src/Symfony/Component/Asset/Packages.php index 3be7c04bae8dc..a3543813fbcdd 100644 --- a/src/Symfony/Component/Asset/Packages.php +++ b/src/Symfony/Component/Asset/Packages.php @@ -121,17 +121,20 @@ public function getUrl($path, $packageName = null) * * @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 = '', $packageName = null) + 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); + return $package->getAndPreloadUrl($path, $as, $nopush); } } diff --git a/src/Symfony/Component/Asset/Preload/HttpFoundationPreloadManager.php b/src/Symfony/Component/Asset/Preload/HttpFoundationPreloadManager.php index 08de3d1f3339a..77d21a3a0f2cb 100644 --- a/src/Symfony/Component/Asset/Preload/HttpFoundationPreloadManager.php +++ b/src/Symfony/Component/Asset/Preload/HttpFoundationPreloadManager.php @@ -10,6 +10,8 @@ */ namespace Symfony\Component\Asset\Preload; + +use Symfony\Component\Asset\Exception\InvalidArgumentException; use Symfony\Component\HttpFoundation\Response; /** @@ -24,9 +26,9 @@ class HttpFoundationPreloadManager implements PreloadManagerInterface /** * {@inheritdoc} */ - public function addResource($uri, $as) + public function addResource($uri, $as = '', $nopush = false) { - $this->resources[$uri] = $as; + $this->resources[$uri] = array('as' => $as, 'nopush' => $nopush); } /** @@ -42,6 +44,20 @@ public function getResources() */ public function setResources(array $resources) { + foreach ($resources as $key => $options) { + if (!is_string($key)) { + throw new InvalidArgumentException('The key must be a path to an asset, "%s" given.', $key); + } + + if (!isset($options['as']) || !is_string($options['as'])) { + throw new InvalidArgumentException('The "as" option is mandatory and must be a string.'); + } + + if (!isset($options['nopush']) || !is_bool($options['nopush'])) { + throw new InvalidArgumentException('The "nopush" option is mandatory and must be a bool.'); + } + } + $this->resources = $resources; } @@ -57,8 +73,17 @@ public function setLinkHeader(Response $response) } $parts = array(); - foreach ($this->resources as $uri => $as) { - $parts[] = "<$uri>; rel=preload; as=$as"; + foreach ($this->resources as $uri => $options) { + $part = "<$uri>; rel=preload"; + if ('' !== $options['as']) { + $part .= "; as={$options['as']}"; + } + + if ($options['nopush']) { + $part .= '; nopush'; + } + + $parts[] = $part; } $response->headers->set('Link', implode(',', $parts)); diff --git a/src/Symfony/Component/Asset/Preload/PreloadManagerInterface.php b/src/Symfony/Component/Asset/Preload/PreloadManagerInterface.php index 7cd5a21eef5fd..6dc6028dc3fd7 100644 --- a/src/Symfony/Component/Asset/Preload/PreloadManagerInterface.php +++ b/src/Symfony/Component/Asset/Preload/PreloadManagerInterface.php @@ -15,6 +15,7 @@ * Manages resources to preload according to the W3C "Preload" specification. * * @see https://www.w3.org/TR/preload/ + * * @author Kévin Dunglas */ interface PreloadManagerInterface @@ -22,10 +23,11 @@ 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 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); + public function addResource($uri, $as = '', $nopush = false); /** * Gets the list of resources to preload. diff --git a/src/Symfony/Component/Asset/PreloadedPackageInterface.php b/src/Symfony/Component/Asset/PreloadedPackageInterface.php index 856ade4798f7f..122080ba4bad1 100644 --- a/src/Symfony/Component/Asset/PreloadedPackageInterface.php +++ b/src/Symfony/Component/Asset/PreloadedPackageInterface.php @@ -21,10 +21,10 @@ interface PreloadedPackageInterface extends PackageInterface /** * Returns an absolute or root-relative public path and adds it to the URLs to preload. * - * @param string $path A path - * @param string $as A valid destination according to https://fetch.spec.whatwg.org/#concept-request-destination - * + * @param string $path A 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 */ - public function getAndPreloadUrl($path, $as = ''); + public function getAndPreloadUrl($path, $as = '', $nopush = false); } diff --git a/src/Symfony/Component/Asset/Tests/EventListener/PreloadListenerTest.php b/src/Symfony/Component/Asset/Tests/EventListener/PreloadListenerTest.php new file mode 100644 index 0000000000000..a352c02c66056 --- /dev/null +++ b/src/Symfony/Component/Asset/Tests/EventListener/PreloadListenerTest.php @@ -0,0 +1,38 @@ + + * + * 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\HttpFoundationPreloadManager; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; + +/** + * @author Kévin Dunglas + */ +class PreloadListenerTest extends \PHPUnit_Framework_TestCase +{ + public function testOnKernelResponse() + { + $manager = new HttpFoundationPreloadManager(); + $manager->addResource('/foo'); + + $listener = new PreloadListener($manager); + $response = new Response(); + + $event = $this->createMock(FilterResponseEvent::class); + $event->method('getResponse')->willReturn($response); + + $listener->onKernelResponse($event); + $this->assertEquals('; rel=preload', $response->headers->get('Link')); + } +} diff --git a/src/Symfony/Component/Asset/Tests/PackageTest.php b/src/Symfony/Component/Asset/Tests/PackageTest.php index a2310d5898dee..c0dc50b1cdeba 100644 --- a/src/Symfony/Component/Asset/Tests/PackageTest.php +++ b/src/Symfony/Component/Asset/Tests/PackageTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Asset\Tests; use Symfony\Component\Asset\Package; +use Symfony\Component\Asset\Preload\PreloadManagerInterface; use Symfony\Component\Asset\VersionStrategy\StaticVersionStrategy; use Symfony\Component\Asset\VersionStrategy\EmptyVersionStrategy; @@ -51,4 +52,21 @@ public function testGetVersion() $package = new Package(new StaticVersionStrategy('v1')); $this->assertEquals('v1', $package->getVersion('/foo')); } + + public function testGetAndPreloadUrl() + { + $preloadManager = $this->createMock(PreloadManagerInterface::class); + $preloadManager + ->expects($this->exactly(2)) + ->method('addResource') + ->withConsecutive( + 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)); + } } diff --git a/src/Symfony/Component/Asset/Tests/PackagesTest.php b/src/Symfony/Component/Asset/Tests/PackagesTest.php index bb515f20964f0..4d989a6497f30 100644 --- a/src/Symfony/Component/Asset/Tests/PackagesTest.php +++ b/src/Symfony/Component/Asset/Tests/PackagesTest.php @@ -12,7 +12,9 @@ namespace Symfony\Component\Asset\Tests; use Symfony\Component\Asset\Package; +use Symfony\Component\Asset\PackageInterface; use Symfony\Component\Asset\Packages; +use Symfony\Component\Asset\Preload\PreloadManagerInterface; use Symfony\Component\Asset\VersionStrategy\StaticVersionStrategy; class PackagesTest extends \PHPUnit_Framework_TestCase @@ -54,6 +56,27 @@ public function testGetUrl() $this->assertEquals('/foo?a', $packages->getUrl('/foo', 'a')); } + public function testGetAndPreloadUrl() + { + $preloadManager = $this->createMock(PreloadManagerInterface::class); + $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 */ @@ -71,4 +94,13 @@ public function testUndefinedPackage() $packages = new Packages(); $packages->getPackage('a'); } + + /** + * @expectedException \Symfony\Component\Asset\Exception\InvalidArgumentException + */ + public function testDoesNotSupportPreloading() + { + $packages = new Packages($this->createMock(PackageInterface::class)); + $packages->getAndPreloadUrl('/foo'); + } } diff --git a/src/Symfony/Component/Asset/Tests/Preload/HttpFoundationPreloadManagerTest.php b/src/Symfony/Component/Asset/Tests/Preload/HttpFoundationPreloadManagerTest.php new file mode 100644 index 0000000000000..3453af4145b70 --- /dev/null +++ b/src/Symfony/Component/Asset/Tests/Preload/HttpFoundationPreloadManagerTest.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Asset\Preload; + +use Symfony\Component\HttpFoundation\Response; + +/** + * @author Kévin Dunglas + */ +class HttpFoundationPreloadManagerTest extends \PHPUnit_Framework_TestCase +{ + public function testManageResources() + { + $manager = new HttpFoundationPreloadManager(); + $this->assertInstanceOf(PreloadManagerInterface::class, $manager); + + $manager->setResources(array('/foo/bar.js' => array('as' => 'script', 'nopush' => false))); + $manager->addResource('/foo/baz.css'); + $manager->addResource('/foo/bat.png', 'image', true); + + $this->assertEquals(array( + '/foo/bar.js' => array('as' => 'script', 'nopush' => false), + '/foo/baz.css' => array('as' => '', 'nopush' => false), + '/foo/bat.png' => array('as' => 'image', 'nopush' => true), + ), $manager->getResources()); + + $response = new Response(); + $manager->setLinkHeader($response); + + $this->assertEquals('; rel=preload; as=script,; rel=preload,; rel=preload; as=image; nopush', $response->headers->get('Link')); + } + + /** + * @expectedException \Symfony\Component\Asset\Exception\InvalidArgumentException + * @dataProvider invalidResources + */ + public function testInvalidResources($resources) + { + $manager = new HttpFoundationPreloadManager(); + $manager->setResources($resources); + } + + public function invalidResources() + { + return array( + array(array('foo' => array())), + array(array('foo' => array('as'))), + array(array('foo' => array('nopush'))), + ); + } +} From 1de19cc0eec09c2b8cba966686d18be3247e5593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 31 Jan 2017 22:08:05 +0100 Subject: [PATCH 03/16] CS --- src/Symfony/Component/Asset/PreloadedPackageInterface.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Symfony/Component/Asset/PreloadedPackageInterface.php b/src/Symfony/Component/Asset/PreloadedPackageInterface.php index 122080ba4bad1..552e1a03287f6 100644 --- a/src/Symfony/Component/Asset/PreloadedPackageInterface.php +++ b/src/Symfony/Component/Asset/PreloadedPackageInterface.php @@ -24,6 +24,7 @@ interface PreloadedPackageInterface extends PackageInterface * @param string $path A 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 */ public function getAndPreloadUrl($path, $as = '', $nopush = false); From c5ca4bc73729e03f5394b240e95c4005a7df21b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 31 Jan 2017 23:34:09 +0100 Subject: [PATCH 04/16] Fix tests --- .../DependencyInjection/FrameworkExtensionTest.php | 13 +++++++++++-- .../Tests/EventListener/PreloadListenerTest.php | 2 +- src/Symfony/Component/Asset/Tests/PackageTest.php | 2 +- src/Symfony/Component/Asset/Tests/PackagesTest.php | 4 ++-- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 1121c476d7357..887a85a6c59d6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -15,6 +15,7 @@ use Symfony\Bundle\FullStack; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension; +use Symfony\Component\Asset\EventListener\PreloadListener; use Symfony\Component\Cache\Adapter\ApcuAdapter; use Symfony\Component\Cache\Adapter\ChainAdapter; use Symfony\Component\Cache\Adapter\DoctrineAdapter; @@ -363,8 +364,6 @@ public function testAssets() $package = $container->getDefinition((string) $packages['bar_version_strategy']); $this->assertEquals('assets.custom_version_strategy', (string) $package->getArgument(1)); - - $this->assertTrue($container->hasDefinition('asset.preload_listener')); } public function testAssetsDefaultVersionStrategyAsService() @@ -377,6 +376,16 @@ public function testAssetsDefaultVersionStrategyAsService() $this->assertEquals('assets.custom_version_strategy', (string) $defaultPackage->getArgument(1)); } + public function testAssetHasPreloadListener() + { + if (!class_exists(PreloadListener::class)) { + $this->markTestSkipped('Requires asset 3.3 or superior.'); + } + + $container = $this->createContainerFromFile('assets'); + $this->assertTrue($container->hasDefinition('asset.preload_listener')); + } + public function testTranslator() { $container = $this->createContainerFromFile('full'); diff --git a/src/Symfony/Component/Asset/Tests/EventListener/PreloadListenerTest.php b/src/Symfony/Component/Asset/Tests/EventListener/PreloadListenerTest.php index a352c02c66056..d6bf0d78cde35 100644 --- a/src/Symfony/Component/Asset/Tests/EventListener/PreloadListenerTest.php +++ b/src/Symfony/Component/Asset/Tests/EventListener/PreloadListenerTest.php @@ -29,7 +29,7 @@ public function testOnKernelResponse() $listener = new PreloadListener($manager); $response = new Response(); - $event = $this->createMock(FilterResponseEvent::class); + $event = $this->getMockBuilder(FilterResponseEvent::class)->disableOriginalConstructor()->getMock(); $event->method('getResponse')->willReturn($response); $listener->onKernelResponse($event); diff --git a/src/Symfony/Component/Asset/Tests/PackageTest.php b/src/Symfony/Component/Asset/Tests/PackageTest.php index c0dc50b1cdeba..2da60e29f7bb4 100644 --- a/src/Symfony/Component/Asset/Tests/PackageTest.php +++ b/src/Symfony/Component/Asset/Tests/PackageTest.php @@ -55,7 +55,7 @@ public function testGetVersion() public function testGetAndPreloadUrl() { - $preloadManager = $this->createMock(PreloadManagerInterface::class); + $preloadManager = $this->getMockBuilder(PreloadManagerInterface::class)->getMock(); $preloadManager ->expects($this->exactly(2)) ->method('addResource') diff --git a/src/Symfony/Component/Asset/Tests/PackagesTest.php b/src/Symfony/Component/Asset/Tests/PackagesTest.php index 4d989a6497f30..4b1fcbeaa4d93 100644 --- a/src/Symfony/Component/Asset/Tests/PackagesTest.php +++ b/src/Symfony/Component/Asset/Tests/PackagesTest.php @@ -58,7 +58,7 @@ public function testGetUrl() public function testGetAndPreloadUrl() { - $preloadManager = $this->createMock(PreloadManagerInterface::class); + $preloadManager = $this->getMockBuilder(PreloadManagerInterface::class)->getMock(); $preloadManager ->expects($this->exactly(2)) ->method('addResource') @@ -100,7 +100,7 @@ public function testUndefinedPackage() */ public function testDoesNotSupportPreloading() { - $packages = new Packages($this->createMock(PackageInterface::class)); + $packages = new Packages($this->getMockBuilder(PackageInterface::class)->getMock()); $packages->getAndPreloadUrl('/foo'); } } From 639d7d10f8d743c693394f6e1e0853b2de0f0507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 1 Feb 2017 18:43:30 +0100 Subject: [PATCH 05/16] Preload is now a standalone Twig function --- .../Bridge/Twig/Extension/AssetExtension.php | 43 +++++++++++-------- .../Tests/Extension/AssetExtensionTest.php | 15 +++++-- .../FrameworkExtension.php | 9 +--- .../TwigBundle/Resources/config/twig.xml | 1 + .../Asset/EventListener/PreloadListener.php | 15 ++++++- src/Symfony/Component/Asset/Package.php | 22 +--------- src/Symfony/Component/Asset/Packages.php | 24 ----------- .../Asset/PreloadedPackageInterface.php | 31 ------------- .../EventListener/PreloadListenerTest.php | 14 +++++- .../Component/Asset/Tests/PackageTest.php | 17 -------- .../Component/Asset/Tests/PackagesTest.php | 30 ------------- 11 files changed, 65 insertions(+), 156 deletions(-) delete mode 100644 src/Symfony/Component/Asset/PreloadedPackageInterface.php diff --git a/src/Symfony/Bridge/Twig/Extension/AssetExtension.php b/src/Symfony/Bridge/Twig/Extension/AssetExtension.php index ffc2a2fd4800a..212a7f17adf8c 100644 --- a/src/Symfony/Bridge/Twig/Extension/AssetExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/AssetExtension.php @@ -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. @@ -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) { $this->packages = $packages; + $this->preloadManager = $preloadManager; } /** @@ -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')), ); } @@ -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; } /** diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php index 89ab0c83cb774..8293d9701b7d2 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php @@ -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 @@ -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'); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index ad73a65425ea0..aae279caedb87 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -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); @@ -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; } diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml index 800fc08367fd8..f247da8b8aa9f 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml @@ -70,6 +70,7 @@ + diff --git a/src/Symfony/Component/Asset/EventListener/PreloadListener.php b/src/Symfony/Component/Asset/EventListener/PreloadListener.php index 57c77448b6301..087162eb75fd0 100644 --- a/src/Symfony/Component/Asset/EventListener/PreloadListener.php +++ b/src/Symfony/Component/Asset/EventListener/PreloadListener.php @@ -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 */ -class PreloadListener +class PreloadListener implements EventSubscriberInterface { private $preloadManager; @@ -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'); } } diff --git a/src/Symfony/Component/Asset/Package.php b/src/Symfony/Component/Asset/Package.php index 419c62bd74ef7..644051c321bda 100644 --- a/src/Symfony/Component/Asset/Package.php +++ b/src/Symfony/Component/Asset/Package.php @@ -22,19 +22,16 @@ * * @author Kris Wallsmith * @author Fabien Potencier - * @author Kévin Dunglas */ -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; } /** @@ -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 */ diff --git a/src/Symfony/Component/Asset/Packages.php b/src/Symfony/Component/Asset/Packages.php index a3543813fbcdd..0a8c96354967c 100644 --- a/src/Symfony/Component/Asset/Packages.php +++ b/src/Symfony/Component/Asset/Packages.php @@ -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); - } } diff --git a/src/Symfony/Component/Asset/PreloadedPackageInterface.php b/src/Symfony/Component/Asset/PreloadedPackageInterface.php deleted file mode 100644 index 552e1a03287f6..0000000000000 --- a/src/Symfony/Component/Asset/PreloadedPackageInterface.php +++ /dev/null @@ -1,31 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Asset; - -/** - * Asset package with preloading support interface. - * - * @author Kévin Dunglas - */ -interface PreloadedPackageInterface extends PackageInterface -{ - /** - * Returns an absolute or root-relative public path and adds it to the URLs to preload. - * - * @param string $path A 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 - */ - public function getAndPreloadUrl($path, $as = '', $nopush = false); -} diff --git a/src/Symfony/Component/Asset/Tests/EventListener/PreloadListenerTest.php b/src/Symfony/Component/Asset/Tests/EventListener/PreloadListenerTest.php index d6bf0d78cde35..9c016149535b3 100644 --- a/src/Symfony/Component/Asset/Tests/EventListener/PreloadListenerTest.php +++ b/src/Symfony/Component/Asset/Tests/EventListener/PreloadListenerTest.php @@ -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 @@ -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('; rel=preload', $response->headers->get('Link')); + $this->assertEmpty($manager->getResources()); + } + + public function testSubscribedEvents() + { + $this->assertEquals(array(KernelEvents::RESPONSE => 'onKernelResponse'), PreloadListener::getSubscribedEvents()); } } diff --git a/src/Symfony/Component/Asset/Tests/PackageTest.php b/src/Symfony/Component/Asset/Tests/PackageTest.php index 2da60e29f7bb4..1a41a5ced1f8a 100644 --- a/src/Symfony/Component/Asset/Tests/PackageTest.php +++ b/src/Symfony/Component/Asset/Tests/PackageTest.php @@ -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( - 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)); - } } diff --git a/src/Symfony/Component/Asset/Tests/PackagesTest.php b/src/Symfony/Component/Asset/Tests/PackagesTest.php index 4b1fcbeaa4d93..6b09d893ed12a 100644 --- a/src/Symfony/Component/Asset/Tests/PackagesTest.php +++ b/src/Symfony/Component/Asset/Tests/PackagesTest.php @@ -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 */ @@ -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'); - } } From 9c11a0fd25fc25774867b0c1e4d0fe357cccd262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 1 Feb 2017 18:51:30 +0100 Subject: [PATCH 06/16] Make the preload manager independant of HTTP Foundation --- .../Twig/Tests/Extension/AssetExtensionTest.php | 4 ++-- .../DependencyInjection/FrameworkExtension.php | 6 +++--- .../Asset/EventListener/PreloadListener.php | 8 ++++---- ...ationPreloadManager.php => PreloadManager.php} | 15 ++++++--------- .../Asset/Preload/PreloadManagerInterface.php | 7 +++++++ .../Tests/EventListener/PreloadListenerTest.php | 4 ++-- ...loadManagerTest.php => PreloadManagerTest.php} | 11 ++++------- 7 files changed, 28 insertions(+), 27 deletions(-) rename src/Symfony/Component/Asset/Preload/{HttpFoundationPreloadManager.php => PreloadManager.php} (83%) rename src/Symfony/Component/Asset/Tests/Preload/{HttpFoundationPreloadManagerTest.php => PreloadManagerTest.php} (83%) diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php index 8293d9701b7d2..0565ed5d61ecd 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php @@ -13,7 +13,7 @@ use Symfony\Bridge\Twig\Extension\AssetExtension; use Symfony\Component\Asset\Packages; -use Symfony\Component\Asset\Preload\HttpFoundationPreloadManager; +use Symfony\Component\Asset\Preload\PreloadManager; /** * @author Kévin Dunglas @@ -22,7 +22,7 @@ class AssetExtensionTest extends \PHPUnit_Framework_TestCase { public function testGetAndPreloadAssetUrl() { - $preloadManager = new HttpFoundationPreloadManager(); + $preloadManager = new PreloadManager(); $extension = new AssetExtension(new Packages(), $preloadManager); $this->assertEquals('/foo.css', $extension->preload('/foo.css', 'style', true)); diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index aae279caedb87..b91757ca10eaf 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -14,7 +14,7 @@ use Doctrine\Common\Annotations\Reader; use Symfony\Bridge\Monolog\Processor\DebugProcessor; use Symfony\Component\Asset\EventListener\PreloadListener; -use Symfony\Component\Asset\Preload\HttpFoundationPreloadManager; +use Symfony\Component\Asset\Preload\PreloadManager; use Symfony\Component\Cache\Adapter\AdapterInterface; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\Alias; @@ -766,8 +766,8 @@ private function registerAssetsConfiguration(array $config, ContainerBuilder $co $defaultVersion = $this->createVersion($container, $config['version'], $config['version_format'], '_default'); } - if (class_exists(HttpFoundationPreloadManager::class)) { - $preloadManagerDefinition = $container->register('assets.preload_manager', HttpFoundationPreloadManager::class); + if (class_exists(PreloadManager::class)) { + $preloadManagerDefinition = $container->register('assets.preload_manager', PreloadManager::class); $preloadManagerDefinition->setPublic(false); $preloadListener = $container->register('asset.preload_listener', PreloadListener::class); diff --git a/src/Symfony/Component/Asset/EventListener/PreloadListener.php b/src/Symfony/Component/Asset/EventListener/PreloadListener.php index 087162eb75fd0..ab0b9f05e4b93 100644 --- a/src/Symfony/Component/Asset/EventListener/PreloadListener.php +++ b/src/Symfony/Component/Asset/EventListener/PreloadListener.php @@ -11,13 +11,13 @@ namespace Symfony\Component\Asset\EventListener; -use Symfony\Component\Asset\Preload\HttpFoundationPreloadManager; +use Symfony\Component\Asset\Preload\PreloadManager; 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. + * Adds the preload Link HTTP header to the response. * * @author Kévin Dunglas */ @@ -25,14 +25,14 @@ class PreloadListener implements EventSubscriberInterface { private $preloadManager; - public function __construct(HttpFoundationPreloadManager $preloadManager) + public function __construct(PreloadManager $preloadManager) { $this->preloadManager = $preloadManager; } public function onKernelResponse(FilterResponseEvent $event) { - $this->preloadManager->setLinkHeader($event->getResponse()); + $event->getResponse()->headers->set('Link', $this->preloadManager->getLinkValue()); // Free memory $this->preloadManager->setResources(array()); diff --git a/src/Symfony/Component/Asset/Preload/HttpFoundationPreloadManager.php b/src/Symfony/Component/Asset/Preload/PreloadManager.php similarity index 83% rename from src/Symfony/Component/Asset/Preload/HttpFoundationPreloadManager.php rename to src/Symfony/Component/Asset/Preload/PreloadManager.php index 77d21a3a0f2cb..5eeaeed957904 100644 --- a/src/Symfony/Component/Asset/Preload/HttpFoundationPreloadManager.php +++ b/src/Symfony/Component/Asset/Preload/PreloadManager.php @@ -12,14 +12,13 @@ namespace Symfony\Component\Asset\Preload; use Symfony\Component\Asset\Exception\InvalidArgumentException; -use Symfony\Component\HttpFoundation\Response; /** - * Preload manager for the HttpFoundation component. + * Manages preload HTTP headers. * * @author Kévin Dunglas */ -class HttpFoundationPreloadManager implements PreloadManagerInterface +class PreloadManager implements PreloadManagerInterface { private $resources = array(); @@ -62,14 +61,12 @@ public function setResources(array $resources) } /** - * Sets preload Link HTTP header. - * - * @param Response $response + * {@inheritdoc} */ - public function setLinkHeader(Response $response) + public function getLinkValue() { if (!$this->resources) { - return; + return null; } $parts = array(); @@ -86,6 +83,6 @@ public function setLinkHeader(Response $response) $parts[] = $part; } - $response->headers->set('Link', implode(',', $parts)); + return implode(',', $parts); } } diff --git a/src/Symfony/Component/Asset/Preload/PreloadManagerInterface.php b/src/Symfony/Component/Asset/Preload/PreloadManagerInterface.php index 6dc6028dc3fd7..6d4dc3464acd2 100644 --- a/src/Symfony/Component/Asset/Preload/PreloadManagerInterface.php +++ b/src/Symfony/Component/Asset/Preload/PreloadManagerInterface.php @@ -42,4 +42,11 @@ public function getResources(); * @param array $resources */ public function setResources(array $resources); + + /** + * Gets the value of the preload Link HTTP header. + * + * @return string|null + */ + public function getLinkValue(); } diff --git a/src/Symfony/Component/Asset/Tests/EventListener/PreloadListenerTest.php b/src/Symfony/Component/Asset/Tests/EventListener/PreloadListenerTest.php index 9c016149535b3..ea2038d8bc60b 100644 --- a/src/Symfony/Component/Asset/Tests/EventListener/PreloadListenerTest.php +++ b/src/Symfony/Component/Asset/Tests/EventListener/PreloadListenerTest.php @@ -12,7 +12,7 @@ namespace Symfony\Component\Asset\Tests\EventListener; use Symfony\Component\Asset\EventListener\PreloadListener; -use Symfony\Component\Asset\Preload\HttpFoundationPreloadManager; +use Symfony\Component\Asset\Preload\PreloadManager; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; @@ -25,7 +25,7 @@ class PreloadListenerTest extends \PHPUnit_Framework_TestCase { public function testOnKernelResponse() { - $manager = new HttpFoundationPreloadManager(); + $manager = new PreloadManager(); $manager->addResource('/foo'); $subscriber = new PreloadListener($manager); diff --git a/src/Symfony/Component/Asset/Tests/Preload/HttpFoundationPreloadManagerTest.php b/src/Symfony/Component/Asset/Tests/Preload/PreloadManagerTest.php similarity index 83% rename from src/Symfony/Component/Asset/Tests/Preload/HttpFoundationPreloadManagerTest.php rename to src/Symfony/Component/Asset/Tests/Preload/PreloadManagerTest.php index 3453af4145b70..78cf6ceea4271 100644 --- a/src/Symfony/Component/Asset/Tests/Preload/HttpFoundationPreloadManagerTest.php +++ b/src/Symfony/Component/Asset/Tests/Preload/PreloadManagerTest.php @@ -16,11 +16,11 @@ /** * @author Kévin Dunglas */ -class HttpFoundationPreloadManagerTest extends \PHPUnit_Framework_TestCase +class PreloadManagerTest extends \PHPUnit_Framework_TestCase { public function testManageResources() { - $manager = new HttpFoundationPreloadManager(); + $manager = new PreloadManager(); $this->assertInstanceOf(PreloadManagerInterface::class, $manager); $manager->setResources(array('/foo/bar.js' => array('as' => 'script', 'nopush' => false))); @@ -33,10 +33,7 @@ public function testManageResources() '/foo/bat.png' => array('as' => 'image', 'nopush' => true), ), $manager->getResources()); - $response = new Response(); - $manager->setLinkHeader($response); - - $this->assertEquals('; rel=preload; as=script,; rel=preload,; rel=preload; as=image; nopush', $response->headers->get('Link')); + $this->assertEquals('; rel=preload; as=script,; rel=preload,; rel=preload; as=image; nopush', $manager->getLinkValue()); } /** @@ -45,7 +42,7 @@ public function testManageResources() */ public function testInvalidResources($resources) { - $manager = new HttpFoundationPreloadManager(); + $manager = new PreloadManager(); $manager->setResources($resources); } From d39b4f1f46b3c2c463593cdd3871eee5a7b819a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 1 Feb 2017 18:52:38 +0100 Subject: [PATCH 07/16] Fix CS --- src/Symfony/Component/Asset/Package.php | 2 -- src/Symfony/Component/Asset/Tests/PackageTest.php | 1 - src/Symfony/Component/Asset/Tests/PackagesTest.php | 2 -- .../Component/Asset/Tests/Preload/PreloadManagerTest.php | 2 -- 4 files changed, 7 deletions(-) diff --git a/src/Symfony/Component/Asset/Package.php b/src/Symfony/Component/Asset/Package.php index 644051c321bda..77b1c934eb172 100644 --- a/src/Symfony/Component/Asset/Package.php +++ b/src/Symfony/Component/Asset/Package.php @@ -13,8 +13,6 @@ use Symfony\Component\Asset\Context\ContextInterface; use Symfony\Component\Asset\Context\NullContext; -use Symfony\Component\Asset\Exception\LogicException; -use Symfony\Component\Asset\Preload\PreloadManagerInterface; use Symfony\Component\Asset\VersionStrategy\VersionStrategyInterface; /** diff --git a/src/Symfony/Component/Asset/Tests/PackageTest.php b/src/Symfony/Component/Asset/Tests/PackageTest.php index 1a41a5ced1f8a..a2310d5898dee 100644 --- a/src/Symfony/Component/Asset/Tests/PackageTest.php +++ b/src/Symfony/Component/Asset/Tests/PackageTest.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Asset\Tests; use Symfony\Component\Asset\Package; -use Symfony\Component\Asset\Preload\PreloadManagerInterface; use Symfony\Component\Asset\VersionStrategy\StaticVersionStrategy; use Symfony\Component\Asset\VersionStrategy\EmptyVersionStrategy; diff --git a/src/Symfony/Component/Asset/Tests/PackagesTest.php b/src/Symfony/Component/Asset/Tests/PackagesTest.php index 6b09d893ed12a..bb515f20964f0 100644 --- a/src/Symfony/Component/Asset/Tests/PackagesTest.php +++ b/src/Symfony/Component/Asset/Tests/PackagesTest.php @@ -12,9 +12,7 @@ namespace Symfony\Component\Asset\Tests; use Symfony\Component\Asset\Package; -use Symfony\Component\Asset\PackageInterface; use Symfony\Component\Asset\Packages; -use Symfony\Component\Asset\Preload\PreloadManagerInterface; use Symfony\Component\Asset\VersionStrategy\StaticVersionStrategy; class PackagesTest extends \PHPUnit_Framework_TestCase diff --git a/src/Symfony/Component/Asset/Tests/Preload/PreloadManagerTest.php b/src/Symfony/Component/Asset/Tests/Preload/PreloadManagerTest.php index 78cf6ceea4271..69d433f43a91e 100644 --- a/src/Symfony/Component/Asset/Tests/Preload/PreloadManagerTest.php +++ b/src/Symfony/Component/Asset/Tests/Preload/PreloadManagerTest.php @@ -11,8 +11,6 @@ namespace Symfony\Component\Asset\Preload; -use Symfony\Component\HttpFoundation\Response; - /** * @author Kévin Dunglas */ From 581c700551d02b1396f9e3074f1386efbe43e4f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 1 Feb 2017 19:00:13 +0100 Subject: [PATCH 08/16] Fix listener --- src/Symfony/Bridge/Twig/composer.json | 2 +- .../Component/Asset/EventListener/PreloadListener.php | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index e540fec40b543..845649cb251f2 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -20,7 +20,7 @@ "twig/twig": "~1.28|~2.0" }, "require-dev": { - "symfony/asset": "~3.3", + "symfony/asset": "~2.8|~3.0", "symfony/finder": "~2.8|~3.0", "symfony/form": "~3.1.9|^3.2.2", "symfony/http-kernel": "~3.2", diff --git a/src/Symfony/Component/Asset/EventListener/PreloadListener.php b/src/Symfony/Component/Asset/EventListener/PreloadListener.php index ab0b9f05e4b93..1e31455a843eb 100644 --- a/src/Symfony/Component/Asset/EventListener/PreloadListener.php +++ b/src/Symfony/Component/Asset/EventListener/PreloadListener.php @@ -32,10 +32,12 @@ public function __construct(PreloadManager $preloadManager) public function onKernelResponse(FilterResponseEvent $event) { - $event->getResponse()->headers->set('Link', $this->preloadManager->getLinkValue()); + if ($value = $this->preloadManager->getLinkValue()) { + $event->getResponse()->headers->set('Link', $value); - // Free memory - $this->preloadManager->setResources(array()); + // Free memory + $this->preloadManager->setResources(array()); + } } /** From 693107cfe71b32bdc59cef673c9756d3f69f4aff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 1 Feb 2017 20:18:33 +0100 Subject: [PATCH 09/16] Fix @stof's comments --- .../Tests/Extension/AssetExtensionTest.php | 2 +- .../FrameworkExtension.php | 11 ----- .../Resources/config/assets.xml | 8 ++++ .../FrameworkExtensionTest.php | 4 -- .../Bundle/FrameworkBundle/composer.json | 1 + .../Asset/EventListener/PreloadListener.php | 8 +++- .../Asset/Preload/PreloadManager.php | 40 +++---------------- .../Asset/Preload/PreloadManagerInterface.php | 17 ++------ .../EventListener/PreloadListenerTest.php | 3 +- .../Tests/Preload/PreloadManagerTest.php | 29 +------------- 10 files changed, 30 insertions(+), 93 deletions(-) diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php index 0565ed5d61ecd..88d7cb8a740a2 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php @@ -26,7 +26,7 @@ public function testGetAndPreloadAssetUrl() $extension = new AssetExtension(new Packages(), $preloadManager); $this->assertEquals('/foo.css', $extension->preload('/foo.css', 'style', true)); - $this->assertEquals(array('/foo.css' => array('as' => 'style', 'nopush' => true)), $preloadManager->getResources()); + $this->assertEquals('; rel=preload; as=style; nopush', $preloadManager->buildLinkValue()); } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index b91757ca10eaf..4722d11300b2d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -13,8 +13,6 @@ use Doctrine\Common\Annotations\Reader; use Symfony\Bridge\Monolog\Processor\DebugProcessor; -use Symfony\Component\Asset\EventListener\PreloadListener; -use Symfony\Component\Asset\Preload\PreloadManager; use Symfony\Component\Cache\Adapter\AdapterInterface; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\Alias; @@ -766,15 +764,6 @@ private function registerAssetsConfiguration(array $config, ContainerBuilder $co $defaultVersion = $this->createVersion($container, $config['version'], $config['version_format'], '_default'); } - if (class_exists(PreloadManager::class)) { - $preloadManagerDefinition = $container->register('assets.preload_manager', PreloadManager::class); - $preloadManagerDefinition->setPublic(false); - - $preloadListener = $container->register('asset.preload_listener', PreloadListener::class); - $preloadListener->addArgument(new Reference('assets.preload_manager')); - $preloadListener->addTag('kernel.event_subscriber'); - } - $defaultPackage = $this->createPackageDefinition($config['base_path'], $config['base_urls'], $defaultVersion); $container->setDefinition('assets._default_package', $defaultPackage); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.xml index 4f2e1fbf362a0..4d648a82ac235 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.xml @@ -38,5 +38,13 @@ + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 887a85a6c59d6..2e6f4001614e8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -378,10 +378,6 @@ public function testAssetsDefaultVersionStrategyAsService() public function testAssetHasPreloadListener() { - if (!class_exists(PreloadListener::class)) { - $this->markTestSkipped('Requires asset 3.3 or superior.'); - } - $container = $this->createContainerFromFile('assets'); $this->assertTrue($container->hasDefinition('asset.preload_listener')); } diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index e658a3dab6742..f7648c32d39e1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -60,6 +60,7 @@ "conflict": { "phpdocumentor/reflection-docblock": "<3.0", "phpdocumentor/type-resolver": "<0.2.0", + "symfony/asset": "<3.3", "symfony/console": "<3.3" }, "suggest": { diff --git a/src/Symfony/Component/Asset/EventListener/PreloadListener.php b/src/Symfony/Component/Asset/EventListener/PreloadListener.php index 1e31455a843eb..f0249960f4fb7 100644 --- a/src/Symfony/Component/Asset/EventListener/PreloadListener.php +++ b/src/Symfony/Component/Asset/EventListener/PreloadListener.php @@ -32,11 +32,15 @@ public function __construct(PreloadManager $preloadManager) public function onKernelResponse(FilterResponseEvent $event) { - if ($value = $this->preloadManager->getLinkValue()) { + if (!$event->isMasterRequest()) { + return; + } + + if ($value = $this->preloadManager->buildLinkValue()) { $event->getResponse()->headers->set('Link', $value); // Free memory - $this->preloadManager->setResources(array()); + $this->preloadManager->clear(); } } diff --git a/src/Symfony/Component/Asset/Preload/PreloadManager.php b/src/Symfony/Component/Asset/Preload/PreloadManager.php index 5eeaeed957904..289cf2c14905c 100644 --- a/src/Symfony/Component/Asset/Preload/PreloadManager.php +++ b/src/Symfony/Component/Asset/Preload/PreloadManager.php @@ -33,37 +33,15 @@ public function addResource($uri, $as = '', $nopush = false) /** * {@inheritdoc} */ - public function getResources() + public function clear() { - return $this->resources; + $this->resources = array(); } /** * {@inheritdoc} */ - public function setResources(array $resources) - { - foreach ($resources as $key => $options) { - if (!is_string($key)) { - throw new InvalidArgumentException('The key must be a path to an asset, "%s" given.', $key); - } - - if (!isset($options['as']) || !is_string($options['as'])) { - throw new InvalidArgumentException('The "as" option is mandatory and must be a string.'); - } - - if (!isset($options['nopush']) || !is_bool($options['nopush'])) { - throw new InvalidArgumentException('The "nopush" option is mandatory and must be a bool.'); - } - } - - $this->resources = $resources; - } - - /** - * {@inheritdoc} - */ - public function getLinkValue() + public function buildLinkValue() { if (!$this->resources) { return null; @@ -71,16 +49,10 @@ public function getLinkValue() $parts = array(); foreach ($this->resources as $uri => $options) { - $part = "<$uri>; rel=preload"; - if ('' !== $options['as']) { - $part .= "; as={$options['as']}"; - } - - if ($options['nopush']) { - $part .= '; nopush'; - } + $as = '' === $options['as'] ? '' : sprintf('; as=%s', $options['as']); + $nopush = $options['nopush'] ? '; nopush' : ''; - $parts[] = $part; + $parts[] = sprintf('<%s>; rel=preload%s%s', $uri, $as, $nopush); } return implode(',', $parts); diff --git a/src/Symfony/Component/Asset/Preload/PreloadManagerInterface.php b/src/Symfony/Component/Asset/Preload/PreloadManagerInterface.php index 6d4dc3464acd2..455f5a2ef7ee7 100644 --- a/src/Symfony/Component/Asset/Preload/PreloadManagerInterface.php +++ b/src/Symfony/Component/Asset/Preload/PreloadManagerInterface.php @@ -30,23 +30,14 @@ interface PreloadManagerInterface public function addResource($uri, $as = '', $nopush = false); /** - * Gets the list of resources to preload. - * - * @return array - */ - public function getResources(); - - /** - * Replaces the list of resources. - * - * @param array $resources + * Clears the list of resources. */ - public function setResources(array $resources); + public function clear(); /** - * Gets the value of the preload Link HTTP header. + * Builds the value of the preload Link HTTP header. * * @return string|null */ - public function getLinkValue(); + public function buildLinkValue(); } diff --git a/src/Symfony/Component/Asset/Tests/EventListener/PreloadListenerTest.php b/src/Symfony/Component/Asset/Tests/EventListener/PreloadListenerTest.php index ea2038d8bc60b..2091633e961ba 100644 --- a/src/Symfony/Component/Asset/Tests/EventListener/PreloadListenerTest.php +++ b/src/Symfony/Component/Asset/Tests/EventListener/PreloadListenerTest.php @@ -32,13 +32,14 @@ public function testOnKernelResponse() $response = new Response(); $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); $this->assertEquals('; rel=preload', $response->headers->get('Link')); - $this->assertEmpty($manager->getResources()); + $this->assertNull($manager->buildLinkValue()); } public function testSubscribedEvents() diff --git a/src/Symfony/Component/Asset/Tests/Preload/PreloadManagerTest.php b/src/Symfony/Component/Asset/Tests/Preload/PreloadManagerTest.php index 69d433f43a91e..dee268e9cc855 100644 --- a/src/Symfony/Component/Asset/Tests/Preload/PreloadManagerTest.php +++ b/src/Symfony/Component/Asset/Tests/Preload/PreloadManagerTest.php @@ -21,35 +21,10 @@ public function testManageResources() $manager = new PreloadManager(); $this->assertInstanceOf(PreloadManagerInterface::class, $manager); - $manager->setResources(array('/foo/bar.js' => array('as' => 'script', 'nopush' => false))); + $manager->addResource('/foo/bar.js', 'script', false); $manager->addResource('/foo/baz.css'); $manager->addResource('/foo/bat.png', 'image', true); - $this->assertEquals(array( - '/foo/bar.js' => array('as' => 'script', 'nopush' => false), - '/foo/baz.css' => array('as' => '', 'nopush' => false), - '/foo/bat.png' => array('as' => 'image', 'nopush' => true), - ), $manager->getResources()); - - $this->assertEquals('; rel=preload; as=script,; rel=preload,; rel=preload; as=image; nopush', $manager->getLinkValue()); - } - - /** - * @expectedException \Symfony\Component\Asset\Exception\InvalidArgumentException - * @dataProvider invalidResources - */ - public function testInvalidResources($resources) - { - $manager = new PreloadManager(); - $manager->setResources($resources); - } - - public function invalidResources() - { - return array( - array(array('foo' => array())), - array(array('foo' => array('as'))), - array(array('foo' => array('nopush'))), - ); + $this->assertEquals('; rel=preload; as=script,; rel=preload,; rel=preload; as=image; nopush', $manager->buildLinkValue()); } } From 2d8d750c87c0860db5c2d1e01881c3fa0a1744b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 1 Feb 2017 20:19:42 +0100 Subject: [PATCH 10/16] Fix CS --- .../Tests/DependencyInjection/FrameworkExtensionTest.php | 1 - src/Symfony/Component/Asset/Preload/PreloadManager.php | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 2e6f4001614e8..697542f4cc23c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -15,7 +15,6 @@ use Symfony\Bundle\FullStack; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension; -use Symfony\Component\Asset\EventListener\PreloadListener; use Symfony\Component\Cache\Adapter\ApcuAdapter; use Symfony\Component\Cache\Adapter\ChainAdapter; use Symfony\Component\Cache\Adapter\DoctrineAdapter; diff --git a/src/Symfony/Component/Asset/Preload/PreloadManager.php b/src/Symfony/Component/Asset/Preload/PreloadManager.php index 289cf2c14905c..d8223d9055d5f 100644 --- a/src/Symfony/Component/Asset/Preload/PreloadManager.php +++ b/src/Symfony/Component/Asset/Preload/PreloadManager.php @@ -11,8 +11,6 @@ namespace Symfony\Component\Asset\Preload; -use Symfony\Component\Asset\Exception\InvalidArgumentException; - /** * Manages preload HTTP headers. * From 0b7fe140ef7094cc0b13ee74e0a8c9388b6979fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Fri, 3 Feb 2017 08:58:44 +0100 Subject: [PATCH 11/16] Don't replace existing link --- .../Component/Asset/EventListener/PreloadListener.php | 5 +++-- .../Asset/Tests/EventListener/PreloadListenerTest.php | 10 ++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/Asset/EventListener/PreloadListener.php b/src/Symfony/Component/Asset/EventListener/PreloadListener.php index f0249960f4fb7..a50958c0ca2af 100644 --- a/src/Symfony/Component/Asset/EventListener/PreloadListener.php +++ b/src/Symfony/Component/Asset/EventListener/PreloadListener.php @@ -12,6 +12,7 @@ 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; @@ -25,7 +26,7 @@ class PreloadListener implements EventSubscriberInterface { private $preloadManager; - public function __construct(PreloadManager $preloadManager) + public function __construct(PreloadManagerInterface $preloadManager) { $this->preloadManager = $preloadManager; } @@ -37,7 +38,7 @@ public function onKernelResponse(FilterResponseEvent $event) } if ($value = $this->preloadManager->buildLinkValue()) { - $event->getResponse()->headers->set('Link', $value); + $event->getResponse()->headers->set('Link', $value, false); // Free memory $this->preloadManager->clear(); diff --git a/src/Symfony/Component/Asset/Tests/EventListener/PreloadListenerTest.php b/src/Symfony/Component/Asset/Tests/EventListener/PreloadListenerTest.php index 2091633e961ba..809010d3915bf 100644 --- a/src/Symfony/Component/Asset/Tests/EventListener/PreloadListenerTest.php +++ b/src/Symfony/Component/Asset/Tests/EventListener/PreloadListenerTest.php @@ -29,7 +29,7 @@ public function testOnKernelResponse() $manager->addResource('/foo'); $subscriber = new PreloadListener($manager); - $response = new Response(); + $response = new Response('', 200, array('Link' => '; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"')); $event = $this->getMockBuilder(FilterResponseEvent::class)->disableOriginalConstructor()->getMock(); $event->method('isMasterRequest')->willReturn(true); @@ -38,7 +38,13 @@ public function testOnKernelResponse() $subscriber->onKernelResponse($event); $this->assertInstanceOf(EventSubscriberInterface::class, $subscriber); - $this->assertEquals('; rel=preload', $response->headers->get('Link')); + + $expected = array( + '; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"', + '; rel=preload', + ); + + $this->assertEquals($expected, $response->headers->get('Link', null, false)); $this->assertNull($manager->buildLinkValue()); } From dffb7b632495175b894a34eeded137ed2e9300a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Fri, 3 Feb 2017 09:00:46 +0100 Subject: [PATCH 12/16] Fix tests --- .../Bridge/Twig/Tests/Extension/AssetExtensionTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php index 88d7cb8a740a2..0032247fcfc78 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php @@ -22,6 +22,10 @@ class AssetExtensionTest extends \PHPUnit_Framework_TestCase { public function testGetAndPreloadAssetUrl() { + if (!class_exists(PreloadManager::class)) { + $this->markTestSkipped('Requires asset 3.3 or superior.'); + } + $preloadManager = new PreloadManager(); $extension = new AssetExtension(new Packages(), $preloadManager); From 18412e5c6f79c4daddb69c3a32aaba96eef198dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Sat, 4 Feb 2017 11:21:39 +0100 Subject: [PATCH 13/16] Some more fixes --- .../DependencyInjection/FrameworkExtension.php | 18 +++++++++++++----- src/Symfony/Component/Asset/composer.json | 6 ++++-- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 4722d11300b2d..88ac0c0655e9d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -797,15 +797,23 @@ private function createPackageDefinition($basePath, array $baseUrls, Reference $ throw new \LogicException('An asset package cannot have base URLs and base paths.'); } - $package = new ChildDefinition($baseUrls ? 'assets.url_package' : 'assets.path_package'); + if (!$baseUrls) { + $package = new ChildDefinition('assets.path_package'); - $package + return $package + ->setPublic(false) + ->replaceArgument(0, $basePath) + ->replaceArgument(1, $version) + ; + } + + $package = new ChildDefinition('assets.url_package'); + + return $package ->setPublic(false) - ->replaceArgument(0, $baseUrls ?: $basePath) + ->replaceArgument(0, $baseUrls) ->replaceArgument(1, $version) ; - - return $package; } private function createVersion(ContainerBuilder $container, $version, $format, $name) diff --git a/src/Symfony/Component/Asset/composer.json b/src/Symfony/Component/Asset/composer.json index dec9d88a93e56..4eeef16848838 100644 --- a/src/Symfony/Component/Asset/composer.json +++ b/src/Symfony/Component/Asset/composer.json @@ -19,10 +19,12 @@ "php": ">=5.5.9" }, "suggest": { - "symfony/http-foundation": "" + "symfony/http-foundation": "", + "symfony/http-kernel": "" }, "require-dev": { - "symfony/http-foundation": "~2.8|~3.0" + "symfony/http-foundation": "~2.8|~3.0", + "symfony/http-kernel": "~2.8|~3.0" }, "autoload": { "psr-4": { "Symfony\\Component\\Asset\\": "" }, From 78d7b6a83d124f57dd1b7737bc32c2fba2f01464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 7 Feb 2017 11:01:37 +0100 Subject: [PATCH 14/16] Remove HttpKernel from suggest --- src/Symfony/Component/Asset/composer.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Symfony/Component/Asset/composer.json b/src/Symfony/Component/Asset/composer.json index 4eeef16848838..8ed8d9d725212 100644 --- a/src/Symfony/Component/Asset/composer.json +++ b/src/Symfony/Component/Asset/composer.json @@ -19,8 +19,7 @@ "php": ">=5.5.9" }, "suggest": { - "symfony/http-foundation": "", - "symfony/http-kernel": "" + "symfony/http-foundation": "" }, "require-dev": { "symfony/http-foundation": "~2.8|~3.0", From 05134b6b2f16e37432843dc2ad03aab8a2c4bfeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 7 Feb 2017 11:02:21 +0100 Subject: [PATCH 15/16] Remove unseless null --- src/Symfony/Component/Asset/Preload/PreloadManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Asset/Preload/PreloadManager.php b/src/Symfony/Component/Asset/Preload/PreloadManager.php index d8223d9055d5f..b070147a6ca76 100644 --- a/src/Symfony/Component/Asset/Preload/PreloadManager.php +++ b/src/Symfony/Component/Asset/Preload/PreloadManager.php @@ -42,7 +42,7 @@ public function clear() public function buildLinkValue() { if (!$this->resources) { - return null; + return; } $parts = array(); From a7f77b47156fc606458a330d75d4131ec6984be9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 7 Feb 2017 11:03:11 +0100 Subject: [PATCH 16/16] Update skip message --- src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php index 0032247fcfc78..fbc10b939aeea 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php @@ -23,7 +23,7 @@ class AssetExtensionTest extends \PHPUnit_Framework_TestCase public function testGetAndPreloadAssetUrl() { if (!class_exists(PreloadManager::class)) { - $this->markTestSkipped('Requires asset 3.3 or superior.'); + $this->markTestSkipped('Requires Asset 3.3+.'); } $preloadManager = new PreloadManager();