Add a new Link component

This commit is contained in:
Kévin Dunglas 2017-04-04 13:58:36 +02:00 committed by Fabien Potencier
parent bc93526731
commit 053de25edf
33 changed files with 580 additions and 245 deletions

View File

@ -18,9 +18,11 @@
"require": {
"php": ">=5.5.9",
"doctrine/common": "~2.4",
"fig/link-util": "^1.0",
"twig/twig": "~1.32|~2.2",
"psr/cache": "~1.0",
"psr/container": "^1.0",
"psr/link": "^1.0",
"psr/log": "~1.0",
"psr/simple-cache": "^1.0",
"symfony/polyfill-intl-icu": "~1.0",
@ -76,6 +78,7 @@
"symfony/twig-bundle": "self.version",
"symfony/validator": "self.version",
"symfony/var-dumper": "self.version",
"symfony/web-link": "self.version",
"symfony/web-profiler-bundle": "self.version",
"symfony/web-server-bundle": "self.version",
"symfony/workflow": "self.version",

View File

@ -12,7 +12,6 @@
namespace Symfony\Bridge\Twig\Extension;
use Symfony\Component\Asset\Packages;
use Symfony\Component\Asset\Preload\PreloadManagerInterface;
/**
* Twig extension for the Symfony Asset component.
@ -22,12 +21,10 @@ use Symfony\Component\Asset\Preload\PreloadManagerInterface;
class AssetExtension extends \Twig_Extension
{
private $packages;
private $preloadManager;
public function __construct(Packages $packages, PreloadManagerInterface $preloadManager = null)
public function __construct(Packages $packages)
{
$this->packages = $packages;
$this->preloadManager = $preloadManager;
}
/**
@ -38,7 +35,6 @@ class AssetExtension extends \Twig_Extension
return array(
new \Twig_SimpleFunction('asset', array($this, 'getAssetUrl')),
new \Twig_SimpleFunction('asset_version', array($this, 'getAssetVersion')),
new \Twig_SimpleFunction('preload', array($this, 'preload')),
);
}
@ -71,26 +67,6 @@ class AssetExtension extends \Twig_Extension
return $this->packages->getVersion($path, $packageName);
}
/**
* Preloads an asset.
*
* @param string $path A public path
* @param string $as A valid destination according to https://fetch.spec.whatwg.org/#concept-request-destination
* @param bool $nopush If this asset should not be pushed over HTTP/2
*
* @return string The path of the asset
*/
public function preload($path, $as = '', $nopush = false)
{
if (null === $this->preloadManager) {
throw new \RuntimeException('A preload manager must be configured to use the "preload" function.');
}
$this->preloadManager->addResource($path, $as, $nopush);
return $path;
}
/**
* Returns the name of the extension.
*

View File

@ -0,0 +1,137 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Extension;
use Fig\Link\GenericLinkProvider;
use Fig\Link\Link;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Twig extension for the Symfony WebLink component.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class WebLinkExtension extends \Twig_Extension
{
private $requestStack;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
/**
* {@inheritdoc}
*/
public function getFunctions()
{
return array(
new \Twig_SimpleFunction('link', array($this, 'link')),
new \Twig_SimpleFunction('preload', array($this, 'preload')),
new \Twig_SimpleFunction('dns_prefetch', array($this, 'dnsPrefetch')),
new \Twig_SimpleFunction('preconnect', array($this, 'preconnect')),
new \Twig_SimpleFunction('prefetch', array($this, 'prefetch')),
new \Twig_SimpleFunction('prerender', array($this, 'prerender')),
);
}
/**
* Adds a "Link" HTTP header.
*
* @param string $uri The relation URI
* @param string $rel The relation type (e.g. "preload", "prefetch", "prerender" or "dns-prefetch")
* @param array $attributes The attributes of this link (e.g. "array('as' => true)", "array('pr' => 0.5)")
*
* @return string The relation URI
*/
public function link($uri, $rel, array $attributes = array())
{
if (!$request = $this->requestStack->getMasterRequest()) {
return $uri;
}
$link = new Link($rel, $uri);
foreach ($attributes as $key => $value) {
$link = $link->withAttribute($key, $value);
}
$linkProvider = $request->attributes->get('_links', new GenericLinkProvider());
$request->attributes->set('_links', $linkProvider->withLink($link));
return $uri;
}
/**
* Preloads a resource.
*
* @param string $uri A public path
* @param array $attributes The attributes of this link (e.g. "array('as' => true)", "array('crossorigin' => 'use-credentials')")
*
* @return string The path of the asset
*/
public function preload($uri, array $attributes = array())
{
return $this->link($uri, 'preload', $attributes);
}
/**
* Resolves a resource origin as early as possible.
*
* @param string $uri A public path
* @param array $attributes The attributes of this link (e.g. "array('as' => true)", "array('pr' => 0.5)")
*
* @return string The path of the asset
*/
public function dnsPrefetch($uri, array $attributes = array())
{
return $this->link($uri, 'dns-prefetch', $attributes);
}
/**
* Initiates a early connection to a resource (DNS resolution, TCP handshake, TLS negotiation).
*
* @param string $uri A public path
* @param array $attributes The attributes of this link (e.g. "array('as' => true)", "array('pr' => 0.5)")
*
* @return string The path of the asset
*/
public function preconnect($uri, array $attributes = array())
{
return $this->link($uri, 'preconnect', $attributes);
}
/**
* Indicates to the client that it should prefetch this resource.
*
* @param string $uri A public path
* @param array $attributes The attributes of this link (e.g. "array('as' => true)", "array('pr' => 0.5)")
*
* @return string The path of the asset
*/
public function prefetch($uri, array $attributes = array())
{
return $this->link($uri, 'prefetch', $attributes);
}
/**
* Indicates to the client that it should prerender this resource .
*
* @param string $uri A public path
* @param array $attributes The attributes of this link (e.g. "array('as' => true)", "array('pr' => 0.5)")
*
* @return string The path of the asset
*/
public function prerender($uri, array $attributes = array())
{
return $this->link($uri, 'prerender', $attributes);
}
}

View File

@ -1,45 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Tests\Extension;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\Twig\Extension\AssetExtension;
use Symfony\Component\Asset\Packages;
use Symfony\Component\Asset\Preload\PreloadManager;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class AssetExtensionTest extends TestCase
{
public function testGetAndPreloadAssetUrl()
{
if (!class_exists(PreloadManager::class)) {
$this->markTestSkipped('Requires Asset 3.3+.');
}
$preloadManager = new PreloadManager();
$extension = new AssetExtension(new Packages(), $preloadManager);
$this->assertEquals('/foo.css', $extension->preload('/foo.css', 'style', true));
$this->assertEquals('</foo.css>; rel=preload; as=style; nopush', $preloadManager->buildLinkValue());
}
/**
* @expectedException \RuntimeException
*/
public function testNoConfiguredPreloadManager()
{
$extension = new AssetExtension(new Packages());
$extension->preload('/foo.css');
}
}

View File

@ -0,0 +1,92 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Tests\Extension;
use Fig\Link\Link;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\Twig\Extension\WebLinkExtension;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class WebLinkExtensionTest extends TestCase
{
/**
* @var Request
*/
private $request;
/**
* @var WebLinkExtension
*/
private $extension;
protected function setUp()
{
$this->request = new Request();
$requestStack = new RequestStack();
$requestStack->push($this->request);
$this->extension = new WebLinkExtension($requestStack);
}
public function testLink()
{
$this->assertEquals('/foo.css', $this->extension->link('/foo.css', 'preload', array('as' => 'style', 'nopush' => true)));
$link = (new Link('preload', '/foo.css'))->withAttribute('as', 'style')->withAttribute('nopush', true);
$this->assertEquals(array($link), array_values($this->request->attributes->get('_links')->getLinks()));
}
public function testPreload()
{
$this->assertEquals('/foo.css', $this->extension->preload('/foo.css', array('as' => 'style', 'crossorigin' => true)));
$link = (new Link('preload', '/foo.css'))->withAttribute('as', 'style')->withAttribute('crossorigin', true);
$this->assertEquals(array($link), array_values($this->request->attributes->get('_links')->getLinks()));
}
public function testDnsPrefetch()
{
$this->assertEquals('/foo.css', $this->extension->dnsPrefetch('/foo.css', array('as' => 'style', 'crossorigin' => true)));
$link = (new Link('dns-prefetch', '/foo.css'))->withAttribute('as', 'style')->withAttribute('crossorigin', true);
$this->assertEquals(array($link), array_values($this->request->attributes->get('_links')->getLinks()));
}
public function testPreconnect()
{
$this->assertEquals('/foo.css', $this->extension->preconnect('/foo.css', array('as' => 'style', 'crossorigin' => true)));
$link = (new Link('preconnect', '/foo.css'))->withAttribute('as', 'style')->withAttribute('crossorigin', true);
$this->assertEquals(array($link), array_values($this->request->attributes->get('_links')->getLinks()));
}
public function testPrefetch()
{
$this->assertEquals('/foo.css', $this->extension->prefetch('/foo.css', array('as' => 'style', 'crossorigin' => true)));
$link = (new Link('prefetch', '/foo.css'))->withAttribute('as', 'style')->withAttribute('crossorigin', true);
$this->assertEquals(array($link), array_values($this->request->attributes->get('_links')->getLinks()));
}
public function testPrerender()
{
$this->assertEquals('/foo.css', $this->extension->prerender('/foo.css', array('as' => 'style', 'crossorigin' => true)));
$link = (new Link('prerender', '/foo.css'))->withAttribute('as', 'style')->withAttribute('crossorigin', true);
$this->assertEquals(array($link), array_values($this->request->attributes->get('_links')->getLinks()));
}
}

View File

@ -20,6 +20,7 @@
"twig/twig": "~1.28|~2.0"
},
"require-dev": {
"fig/link-util": "^1.0",
"symfony/asset": "~2.8|~3.0",
"symfony/finder": "~2.8|~3.0",
"symfony/form": "^3.2.5",
@ -34,7 +35,8 @@
"symfony/stopwatch": "~2.8|~3.0",
"symfony/console": "~2.8|~3.0",
"symfony/var-dumper": "~2.8.10|~3.1.4|~3.2",
"symfony/expression-language": "~2.8|~3.0"
"symfony/expression-language": "~2.8|~3.0",
"symfony/web-link": "~3.3"
},
"suggest": {
"symfony/finder": "",
@ -48,7 +50,8 @@
"symfony/security": "For using the SecurityExtension",
"symfony/stopwatch": "For using the StopwatchExtension",
"symfony/var-dumper": "For using the DumpExtension",
"symfony/expression-language": "For using the ExpressionExtension"
"symfony/expression-language": "For using the ExpressionExtension",
"symfony/web-link": "For using the WebLinkExtension"
},
"autoload": {
"psr-4": { "Symfony\\Bridge\\Twig\\": "" },

View File

@ -18,10 +18,10 @@ use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Form\Form;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Validator\Validation;
use Symfony\Component\WebLink\HttpHeaderSerializer;
/**
* FrameworkExtension configuration structure.
@ -101,6 +101,7 @@ class Configuration implements ConfigurationInterface
$this->addPropertyInfoSection($rootNode);
$this->addCacheSection($rootNode);
$this->addPhpErrorsSection($rootNode);
$this->addWebLinkSection($rootNode);
return $treeBuilder;
}
@ -806,4 +807,16 @@ class Configuration implements ConfigurationInterface
->end()
;
}
private function addWebLinkSection(ArrayNodeDefinition $rootNode)
{
$rootNode
->children()
->arrayNode('web_link')
->info('web links configuration')
->{!class_exists(FullStack::class) && class_exists(HttpHeaderSerializer::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->end()
->end()
;
}
}

View File

@ -15,8 +15,10 @@ use Doctrine\Common\Annotations\Reader;
use Symfony\Bridge\Monolog\Processor\DebugProcessor;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\Config\Resource\DirectoryResource;
use Symfony\Component\Console\Application;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
@ -28,7 +30,6 @@ use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\Finder\Finder;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\Serializer\Encoder\YamlEncoder;
use Symfony\Component\Serializer\Encoder\CsvEncoder;
@ -36,8 +37,8 @@ use Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory;
use Symfony\Component\Serializer\Normalizer\DataUriNormalizer;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer;
use Symfony\Component\WebLink\HttpHeaderSerializer;
use Symfony\Component\Workflow;
use Symfony\Component\Console\Application;
/**
* FrameworkExtension.
@ -208,6 +209,14 @@ class FrameworkExtension extends Extension
$this->registerPropertyInfoConfiguration($config['property_info'], $container, $loader);
}
if ($this->isConfigEnabled($container, $config['web_link'])) {
if (!class_exists(HttpHeaderSerializer::class)) {
throw new LogicException('WebLink support cannot be enabled as the WebLink component is not installed.');
}
$loader->load('web_link.xml');
}
$this->addAnnotatedClassesToCompile(array(
'**Bundle\\Controller\\',
'**Bundle\\Entity\\',

View File

@ -43,13 +43,5 @@
<argument /> <!-- manifest path -->
</service>
<service id="assets.preload_manager" class="Symfony\Component\Asset\Preload\PreloadManager" public="false" />
<service id="asset.preload_listener" class="Symfony\Component\Asset\EventListener\PreloadListener">
<argument type="service" id="assets.preload_manager" />
<tag name="kernel.event_subscriber" />
</service>
</services>
</container>

View File

@ -14,6 +14,7 @@
<xsd:element name="csrf-protection" type="csrf_protection" minOccurs="0" maxOccurs="1" />
<xsd:element name="esi" type="esi" minOccurs="0" maxOccurs="1" />
<xsd:element name="fragments" type="fragments" minOccurs="0" maxOccurs="1" />
<xsd:element name="web-link" type="web_link" minOccurs="0" maxOccurs="1" />
<xsd:element name="profiler" type="profiler" minOccurs="0" maxOccurs="1" />
<xsd:element name="router" type="router" minOccurs="0" maxOccurs="1" />
<xsd:element name="session" type="session" minOccurs="0" maxOccurs="1" />
@ -62,6 +63,10 @@
<xsd:attribute name="path" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="web_link">
<xsd:attribute name="enabled" type="xsd:boolean" />
</xsd:complexType>
<xsd:complexType name="profiler">
<xsd:all>
<xsd:element name="matcher" type="profiler_matcher" minOccurs="0" maxOccurs="1" />

View File

@ -0,0 +1,14 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="web_link.add_link_header_listener" class="Symfony\Component\WebLink\AddLinkHeaderListener" public="false">
<tag name="kernel.event_subscriber" />
</service>
</services>
</container>

View File

@ -238,6 +238,9 @@ class ConfigurationTest extends TestCase
'log' => true,
'throw' => true,
),
'web_link' => array(
'enabled' => !class_exists(FullStack::class),
),
);
}
}

View File

@ -0,0 +1,5 @@
<?php
$container->loadFromExtension('framework', array(
'web_link' => array('enabled' => true),
));

View File

@ -0,0 +1,12 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:framework="http://symfony.com/schema/dic/symfony"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
<framework:config>
<framework:web-link enabled="true" />
</framework:config>
</container>

View File

@ -0,0 +1,3 @@
framework:
web_link:
enabled: true

View File

@ -408,10 +408,10 @@ abstract class FrameworkExtensionTest extends TestCase
$this->assertEquals('assets.custom_version_strategy', (string) $defaultPackage->getArgument(1));
}
public function testAssetHasPreloadListener()
public function testWebLink()
{
$container = $this->createContainerFromFile('assets');
$this->assertTrue($container->hasDefinition('asset.preload_listener'));
$container = $this->createContainerFromFile('web_link');
$this->assertTrue($container->hasDefinition('web_link.add_link_header_listener'));
}
public function testTranslator()

View File

@ -34,6 +34,7 @@
"doctrine/cache": "~1.0"
},
"require-dev": {
"fig/link-util": "^1.0",
"symfony/asset": "~2.8|~3.0",
"symfony/browser-kit": "~2.8|~3.0",
"symfony/console": "~3.3",
@ -53,6 +54,7 @@
"symfony/workflow": "~3.3",
"symfony/yaml": "~3.2",
"symfony/property-info": "~3.3",
"symfony/web-link": "~3.3",
"doctrine/annotations": "~1.0",
"phpdocumentor/reflection-docblock": "^3.0",
"twig/twig": "~1.26|~2.0",
@ -79,7 +81,8 @@
"symfony/validator": "For using validation",
"symfony/yaml": "For using the debug:config and lint:yaml commands",
"symfony/property-info": "For using the property_info service",
"symfony/process": "For using the server:run, server:start, server:stop, and server:status commands"
"symfony/process": "For using the server:run, server:start, server:stop, and server:status commands",
"symfony/web-link": "For using web links, features such as preloading, prefetching or prerendering"
},
"autoload": {
"psr-4": { "Symfony\\Bundle\\FrameworkBundle\\": "" },

View File

@ -11,11 +11,13 @@
namespace Symfony\Bundle\TwigBundle\DependencyInjection;
use Symfony\Bridge\Twig\Extension\WebLinkExtension;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\WebLink\HttpHeaderSerializer;
/**
* TwigExtension.
@ -48,6 +50,12 @@ class TwigExtension extends Extension
$container->removeDefinition('twig.translation.extractor');
}
if (class_exists(HttpHeaderSerializer::class)) {
$definition = $container->register('twig.extension.weblink', WebLinkExtension::class);
$definition->setPublic(false);
$definition->addArgument(new Reference('request_stack'));
}
foreach ($configs as $key => $config) {
if (isset($config['globals'])) {
foreach ($config['globals'] as $name => $value) {

View File

@ -72,7 +72,6 @@
<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">

View File

@ -34,6 +34,7 @@
"symfony/templating": "~2.8|~3.0",
"symfony/yaml": "~2.8|~3.0",
"symfony/framework-bundle": "^3.2.2",
"symfony/web-link": "~3.3",
"doctrine/annotations": "~1.0"
},
"conflict": {

View File

@ -1,58 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Asset\Preload;
/**
* Manages preload HTTP headers.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class PreloadManager implements PreloadManagerInterface
{
private $resources = array();
/**
* {@inheritdoc}
*/
public function addResource($uri, $as = '', $nopush = false)
{
$this->resources[$uri] = array('as' => $as, 'nopush' => $nopush);
}
/**
* {@inheritdoc}
*/
public function clear()
{
$this->resources = array();
}
/**
* {@inheritdoc}
*/
public function buildLinkValue()
{
if (!$this->resources) {
return;
}
$parts = array();
foreach ($this->resources as $uri => $options) {
$as = '' === $options['as'] ? '' : sprintf('; as=%s', $options['as']);
$nopush = $options['nopush'] ? '; nopush' : '';
$parts[] = sprintf('<%s>; rel=preload%s%s', $uri, $as, $nopush);
}
return implode(',', $parts);
}
}

View File

@ -1,43 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Asset\Preload;
/**
* Manages resources to preload according to the W3C "Preload" specification.
*
* @see https://www.w3.org/TR/preload/
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
interface PreloadManagerInterface
{
/**
* Adds an element to the list of resources to preload.
*
* @param string $uri The resource URI
* @param string $as A valid destination according to https://fetch.spec.whatwg.org/#concept-request-destination
* @param bool $nopush If this asset should not be pushed over HTTP/2
*/
public function addResource($uri, $as = '', $nopush = false);
/**
* Clears the list of resources.
*/
public function clear();
/**
* Builds the value of the preload Link HTTP header.
*
* @return string|null
*/
public function buildLinkValue();
}

View File

@ -1,32 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Asset\Preload;
use PHPUnit\Framework\TestCase;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class PreloadManagerTest extends TestCase
{
public function testManageResources()
{
$manager = new PreloadManager();
$this->assertInstanceOf(PreloadManagerInterface::class, $manager);
$manager->addResource('/foo/bar.js', 'script', false);
$manager->addResource('/foo/baz.css');
$manager->addResource('/foo/bat.png', 'image', true);
$this->assertEquals('</foo/bar.js>; rel=preload; as=script,</foo/baz.css>; rel=preload,</foo/bat.png>; rel=preload; as=image; nopush', $manager->buildLinkValue());
}
}

View File

@ -0,0 +1,3 @@
vendor/
composer.lock
phpunit.xml

View File

@ -0,0 +1,7 @@
CHANGELOG
=========
3.3.0
-----
* added the component

View File

@ -9,26 +9,28 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Asset\EventListener;
namespace Symfony\Component\WebLink\EventListener;
use Symfony\Component\Asset\Preload\PreloadManager;
use Symfony\Component\Asset\Preload\PreloadManagerInterface;
use Psr\Link\LinkProviderInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\WebLink\HttpHeaderSerializer;
/**
* Adds the preload Link HTTP header to the response.
* Adds the Link HTTP header to the response.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @final
*/
class PreloadListener implements EventSubscriberInterface
class AddLinkHeaderListener implements EventSubscriberInterface
{
private $preloadManager;
private $serializer;
public function __construct(PreloadManagerInterface $preloadManager)
public function __construct()
{
$this->preloadManager = $preloadManager;
$this->serializer = new HttpHeaderSerializer();
}
public function onKernelResponse(FilterResponseEvent $event)
@ -37,12 +39,12 @@ class PreloadListener implements EventSubscriberInterface
return;
}
if ($value = $this->preloadManager->buildLinkValue()) {
$event->getResponse()->headers->set('Link', $value, false);
// Free memory
$this->preloadManager->clear();
$linkProvider = $event->getRequest()->attributes->get('_links');
if (!$linkProvider instanceof LinkProviderInterface || !$links = $linkProvider->getLinks()) {
return;
}
$event->getResponse()->headers->set('Link', $this->serializer->serialize($links), false);
}
/**

View File

@ -0,0 +1,66 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\WebLink;
use Psr\Link\LinkInterface;
/**
* Serializes a list of Link instances to a HTTP Link header.
*
* @see https://tools.ietf.org/html/rfc5988
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
final class HttpHeaderSerializer
{
/**
* Builds the value of the "Link" HTTP header.
*
* @param LinkInterface[]|\Traversable $links
*
* @return string|null
*/
public function serialize($links)
{
$elements = array();
foreach ($links as $link) {
if ($link->isTemplated()) {
continue;
}
$attributesParts = array('', sprintf('rel="%s"', implode(' ', $link->getRels())));
foreach ($link->getAttributes() as $key => $value) {
if (is_array($value)) {
foreach ($value as $v) {
$attributesParts[] = sprintf('%s="%s"', $key, $v);
}
continue;
}
if (!is_bool($value)) {
$attributesParts[] = sprintf('%s="%s"', $key, $value);
continue;
}
if (true === $value) {
$attributesParts[] = $key;
}
}
$elements[] = sprintf('<%s>%s', $link->getHref(), implode('; ', $attributesParts));
}
return $elements ? implode(',', $elements) : null;
}
}

View File

@ -0,0 +1,19 @@
Copyright (c) 2004-2017 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,18 @@
WebLink Component
=================
The WebLink component manages links between resources. It is particularly useful to advise clients
to preload and prefetch documents through HTTP and HTTP/2 pushes.
This component implements the [HTML5's Links](https://www.w3.org/TR/html5/links.html), [Preload](https://www.w3.org/TR/preload/)
and [Resource Hints](https://www.w3.org/TR/resource-hints/) W3C's specifications.
It can also be used with extensions defined in the [HTML5 link type extensions wiki](http://microformats.org/wiki/existing-rel-values#HTML5_link_type_extensions).
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/weblink/introduction.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)

View File

@ -9,11 +9,13 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Asset\Tests\EventListener;
namespace Symfony\Component\WebLink\Tests\EventListener;
use Fig\Link\GenericLinkProvider;
use Fig\Link\Link;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Asset\EventListener\PreloadListener;
use Symfony\Component\Asset\Preload\PreloadManager;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\WebLink\EventListener\AddLinkHeaderListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
@ -22,18 +24,18 @@ use Symfony\Component\HttpKernel\KernelEvents;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class PreloadListenerTest extends TestCase
class AddLinkHeaderListenerTest extends TestCase
{
public function testOnKernelResponse()
{
$manager = new PreloadManager();
$manager->addResource('/foo');
$subscriber = new PreloadListener($manager);
$request = new Request(array(), array(), array('_links' => new GenericLinkProvider(array(new Link('preload', '/foo')))));
$response = new Response('', 200, array('Link' => '<https://demo.api-platform.com/docs.jsonld>; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"'));
$subscriber = new AddLinkHeaderListener();
$event = $this->getMockBuilder(FilterResponseEvent::class)->disableOriginalConstructor()->getMock();
$event->method('isMasterRequest')->willReturn(true);
$event->method('getRequest')->willReturn($request);
$event->method('getResponse')->willReturn($response);
$subscriber->onKernelResponse($event);
@ -42,15 +44,14 @@ class PreloadListenerTest extends TestCase
$expected = array(
'<https://demo.api-platform.com/docs.jsonld>; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"',
'</foo>; rel=preload',
'</foo>; rel="preload"',
);
$this->assertEquals($expected, $response->headers->get('Link', null, false));
$this->assertNull($manager->buildLinkValue());
}
public function testSubscribedEvents()
{
$this->assertEquals(array(KernelEvents::RESPONSE => 'onKernelResponse'), PreloadListener::getSubscribedEvents());
$this->assertEquals(array(KernelEvents::RESPONSE => 'onKernelResponse'), AddLinkHeaderListener::getSubscribedEvents());
}
}

View File

@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\WebLink\Tests;
use Fig\Link\GenericLinkProvider;
use Fig\Link\Link;
use PHPUnit\Framework\TestCase;
use Symfony\Component\WebLink\HttpHeaderSerializer;
class HttpHeaderSerializerTest extends TestCase
{
/**
* @var HttpHeaderSerializer
*/
private $serializer;
protected function setUp()
{
$this->serializer = new HttpHeaderSerializer();
}
public function testSerialize()
{
$links = array(
new Link('prerender', '/1'),
(new Link('dns-prefetch', '/2'))->withAttribute('pr', 0.7),
(new Link('preload', '/3'))->withAttribute('as', 'script')->withAttribute('nopush', false),
(new Link('preload', '/4'))->withAttribute('as', 'image')->withAttribute('nopush', true),
(new Link('alternate', '/5'))->withRel('next')->withAttribute('hreflang', array('fr', 'de'))->withAttribute('title', 'Hello'),
);
$this->assertEquals('</1>; rel="prerender",</2>; rel="dns-prefetch"; pr="0.7",</3>; rel="preload"; as="script",</4>; rel="preload"; as="image"; nopush,</5>; rel="alternate next"; hreflang="fr"; hreflang="de"; title="Hello"', $this->serializer->serialize($links));
}
public function testSerializeEmpty()
{
$this->assertNull($this->serializer->serialize(array()));
}
}

View File

@ -0,0 +1,43 @@
{
"name": "symfony/web-link",
"type": "library",
"description": "Symfony WebLink Component",
"keywords": ["link", "psr13", "http", "HTTP/2", "preload", "prefetch", "prerender", "dns-prefetch", "push", "performance"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Kévin Dunglas",
"email": "dunglas@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=5.5.9",
"fig/link-util": "^1.0",
"psr/link": "^1.0"
},
"suggest": {
"symfony/http-kernel": ""
},
"require-dev": {
"symfony/event-dispatcher": "^2.8|^3.0",
"symfony/http-foundation": "^2.8|^3.0",
"symfony/http-kernel": "^2.8|^3.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Link\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "3.3-dev"
}
}
}

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
>
<php>
<ini name="error_reporting" value="-1" />
</php>
<testsuites>
<testsuite name="Symfony WebLink Component Test Suite">
<directory>./Tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./</directory>
<exclude>
<directory>./Tests</directory>
<directory>./vendor</directory>
</exclude>
</whitelist>
</filter>
</phpunit>