8000 feature #22273 Add a new Link component (dunglas) · symfony/symfony@5a76834 · GitHub
[go: up one dir, main page]

Skip to content

Commit 5a76834

Browse files
committed
feature #22273 Add a new Link component (dunglas)
This PR was squashed before being merged into the 3.3-dev branch (closes #22273). Discussion ---------- Add a new Link component | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? |no <!-- don't forget updating src/**/CHANGELOG.md files --> | BC breaks? | no | Deprecations? | no <!-- don't forget updating UPGRADE-*.md files --> | Tests pass? | yes | Fixed tickets | n/a | License | MIT | Doc PR | todo This a proposal to extract HTTP preloading features introduced in #21478 in their own component. There are some good reasons to do it: * HTTP preloading is not (only) about assets: this standalone component could be very useful to replace resources embedding in APIs by HTTP/2 pushes like described in [this article](https://evertpot.com/rest-embedding-hal-http2/) by @evert. In such case, there is no reason to carry the whole asset component for an API. * There is no dependency nor relation at all between the code of the asset compnent and the one I've added for Preloading features * It makes the code cleaner (no more optional dependency in the `Asset` Twig extension) This component would also better fit in HttpFoundation than in Asset. But there is no dependency between it and HttpFoundation and it can easily be used with PSR-7 too, so IMO it better belongs in a standalone component. Btw, ~~~I plan to add support for prefetching to this component. Except a PR soon.~~~ Prefetching and prerendering support added in this PR. ping @symfony/deciders Commits ------- 053de25 Add a new Link component
2 parents aada1a1 + 053de25 commit 5a76834

33 files changed

+580
-245
lines changed

composer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@
1818
"require": {
1919
"php": ">=5.5.9",
2020
"doctrine/common": "~2.4",
21+
"fig/link-util": "^1.0",
2122
"twig/twig": "~1.32|~2.2",
2223
"psr/cache": "~1.0",
2324
"psr/container": "^1.0",
25+
"psr/link": "^1.0",
2426
"psr/log": "~1.0",
2527
"psr/simple-cache": "^1.0",
2628
"symfony/polyfill-intl-icu": "~1.0",
@@ -76,6 +78,7 @@
7678
"symfony/twig-bundle": "self.version",
7779
"symfony/validator": "self.version",
7880
"symfony/var-dumper": "self.version",
81+
"symfony/web-link": "self.version",
7982
"symfony/web-profiler-bundle": "self.version",
8083
"symfony/web-server-bundle": "self.version",
8184
"symfony/workflow": "self.version",

src/Symfony/Bridge/Twig/Extension/AssetExtension.php

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
namespace Symfony\Bridge\Twig\Extension;
1313

1414
use Symfony\Component\Asset\Packages;
15-
use Symfony\Component\Asset\Preload\PreloadManagerInterface;
1615

1716
/**
1817
* Twig extension for the Symfony Asset component.
@@ -22,12 +21,10 @@
2221
class AssetExtension extends \Twig_Extension
2322
{
2423
private $packages;
25-
private $preloadManager;
2624

27-
public function __construct(Packages $packages, PreloadManagerInterface $preloadManager = null)
25+
public function __construct(Packages $packages)
2826
{
2927
$this->packages = $packages;
30-
$this->preloadManager = $preloadManager;
3128
}
3229

3330
/**
@@ -38,7 +35,6 @@ public function getFunctions()
3835
return array(
3936
new \Twig_SimpleFunction('asset', array($this, 'getAssetUrl')),
4037
new \Twig_SimpleFunction('asset_version', array($this, 'getAssetVersion')),
41-
new \Twig_SimpleFunction('preload', array($this, 'preload')),
4238
);
4339
}
4440

@@ -71,26 +67,6 @@ public function getAssetVersion($path, $packageName = null)
7167
return $this->packages->getVersion($path, $packageName);
7268
}
7369

74-
/**
75-
* Preloads an asset.
76-
*
77-
* @param string $path A public path
78-
* @param string $as A valid destination according to https://fetch.spec.whatwg.org/#concept-request-destination
79-
* @param bool $nopush If this asset should not be pushed over HTTP/2
80-
*
81-
* @return string The path of the asset
82-
*/
83-
public function preload($path, $as = '', $nopush = false)
84-
{
85-
if (null === $this->preloadManager) {
86-
throw new \RuntimeException('A preload manager must be configured to use the "preload" function.');
87-
}
88-
89-
$this->preloadManager->addResource($path, $as, $nopush);
90-
91-
return $path;
92-
}
93-
9470
/**
9571
* Returns the name of the extension.
9672
*
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Twig\Extension;
13+
14+
use Fig\Link\GenericLinkProvider;
15+
use Fig\Link\Link;
16+
use Symfony\Component\HttpFoundation\RequestStack;
17+
18+
/**
19+
* Twig extension for the Symfony WebLink component.
20+
*
21+
* @author Kévin Dunglas <dunglas@gmail.com>
22+
*/
23+
class WebLinkExtension extends \Twig_Extension
24+
{
25+
private $requestStack;
26+
27+
public function __construct(RequestStack $requestStack)
28+
{
29+
$this->requestStack = $requestStack;
30+
}
31+
32+
/**
33+
* {@inheritdoc}
34+
*/
35+
public function getFunctions()
36+
{
37+
return array(
38+
new \Twig_SimpleFunction('link', array($this, 'link')),
39+
new \Twig_SimpleFunction('preload', array($this, 'preload')),
40+
new \Twig_SimpleFunction('dns_prefetch', array($this, 'dnsPrefetch')),
41+
new \Twig_SimpleFunction('preconnect', array($this, 'preconnect')),
42+
new \Twig_SimpleFunction('prefetch', array($this, 'prefetch')),
43+
new \Twig_SimpleFunction('prerender', array($this, 'prerender')),
44+
);
45+
}
46+
47+
/**
48+
* Adds a "Link" HTTP header.
49+
*
50+
* @param string $uri The relation URI
51+
* @param string $rel The relation type (e.g. "preload", "prefetch", "prerender" or "dns-prefetch")
52+
* @param array $attributes The attributes of this link (e.g. "array('as' => true)", "array('pr' => 0.5)")
53+
*
54+
* @return string The relation URI
55+
*/
56+
public function link($uri, $rel, array $attributes = array())
57+
{
58+
if (!$request = $this->requestStack->getMasterRequest()) {
59+
return $uri;
60+
}
61+
62+
$link = new Link($rel, $uri);
63+
foreach ($attributes as $key => $value) {
64+
$link = $link->withAttribute($key, $value);
65+
}
66+
67+
$linkProvider = $request->attributes->get('_links', new GenericLinkProvider());
68+
$request->attributes->set('_links', $linkProvider->withLink($link));
69+
70+
return $uri;
71+
}
72+
73+
/**
74+
* Preloads a resource.
75+
*
76+
* @param string $uri A public path
77+
* @param array $attributes The attributes of this link (e.g. "array('as' => true)", "array('crossorigin' => 'use-credentials')")
78+
*
79+
* @return string The path of the asset
80+
*/
81+
public function preload($uri, array $attributes = array())
82+
{
83+
return $this->link($uri, 'preload', $attributes);
84+
}
85+
86+
/**
87+
* Resolves a resource origin as early as possible.
88+
*
89+
* @param string $uri A public path
90+
* @param array $attributes The attributes of this link (e.g. "array('as' => true)", "array('pr' => 0.5)")
91+
*
92+
* @return string The path of the asset
93+
*/
94+
public function dnsPrefetch($uri, array $attributes = array())
95+
{
96+
return $this->link($uri, 'dns-prefetch', $attributes);
97+
}
98+
99+
/**
100+
* Initiates a early connection to a resource (DNS resolution, TCP handshake, TLS negotiation).
101+
*
102+
* @param string $uri A public path
103+
* @param array $attributes The attributes of this link (e.g. "array('as' => true)", "array('pr' => 0.5)")
104+
*
105+
* @return string The path of the asset
106+
*/
107+
public function preconnect($uri, array $attributes = array())
108+
{
109+
return $this->link($uri, 'preconnect', $attributes);
110+
}
111+
112+
/**
113+
* Indicates to the client that it should prefetch this resource.
114+
*
115+
* @param string $uri A public path
116+
* @param array $attributes The attributes of this link (e.g. "array('as' => true)", "array('pr' => 0.5)")
117+
*
118+
* @return string The path of the asset
119+
*/
120+
public function prefetch($uri, array $attributes = array())
121+
{
122+
return $this->link($uri, 'prefetch', $attributes);
123+
}
124+
125+
/**
126+
* Indicates to the client that it should prerender this resource .
127+
*
128+
* @param string $uri A public path
129+
* @param array $attributes The attributes of this link (e.g. "array('as' => true)", "array('pr' => 0.5)")
130+
*
131+
* @return string The path of the asset
132+
*/
133+
public function prerender($uri, array $attributes = array())
134+
{
135+
return $this->link($uri, 'prerender', $attributes);
136+
}
137+
}

src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php

Lines changed: 0 additions & 45 deletions
This file was deleted.
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\Twig\Tests\Extension;
13+
14+
use Fig\Link\Link;
15+
use PHPUnit\Framework\TestCase;
16+
use Symfony\Bridge\Twig\Extension\WebLinkExtension;
17+
use Symfony\Component\HttpFoundation\Request;
18+
use Symfony\Component\HttpFoundation\RequestStack;
19+
20+
/**
21+
* @author Kévin Dunglas <dunglas@gmail.com>
22+
*/
23+
class WebLinkExtensionTest extends TestCase
24+
{
25+
/**
26+
* @var Request
27+
*/
28+
private $request;
29+
30+
/**
31+
* @var WebLinkExtension
32+
*/
33+
private $extension;
34+
35+
protected function setUp()
36+
{
37+
$this->request = new Request();
38+
39+
$requestStack = new RequestStack();
40+
$requestStack->push($this->request);
41+
42+
$this->extension = new WebLinkExtension($requestStack);
43+
}
44+
45+
public function testLink()
46+
{
47+
$this->assertEquals('/foo.css', $this->extension->link('/foo.css', 'preload', array('as' => 'style', 'nopush' => true)));
48+
49+
$link = (new Link('preload', '/foo.css'))->withAttribute('as', 'style')->withAttribute('nopush', true);
50+
$this->assertEquals(array($link), array_values($this->request->attributes->get('_links')->getLinks()));
51+
}
52+
53+
public function testPreload()
54+
{
55+
$this->assertEquals('/foo.css', $this->extension->preload('/foo.css', array('as' => 'style', 'crossorigin' => true)));
56+
57+
$link = (new Link('preload', '/foo.css'))->withAttribute('as', 'style')->withAttribute('crossorigin', true);
58+
$this->assertEquals(array($link), array_values($this->request->attributes->get('_links')->getLinks()));
59+
}
60+
61+
public function testDnsPrefetch()
62+
{
63+
$this->assertEquals('/foo.css', $this->extension->dnsPrefetch('/foo.css', array('as' => 'style', 'crossorigin' => true)));
64+
65+
$link = (new Link('dns-prefetch', '/foo.css'))->withAttribute('as', 'style')->withAttribute('crossorigin', true);
66+
$this->assertEquals(array($link), array_values($this->request->attributes->get('_links')->getLinks()));
67+
}
68+
69+
public function testPreconnect()
70+
{
71+
$this->assertEquals('/foo.css', $this->extension->preconnect('/foo.css', array('as' => 'style', 'crossorigin' => true)));
72+
73+
$link = (new Link('preconnect', '/foo.css'))->withAttribute('as', 'style')->withAttribute('crossorigin', true);
74+
$this->assertEquals(array($link), array_values($this->request->attributes->get('_links')->getLinks()));
75+
}
76+
77+
public function testPrefetch()
78+
{
79+
$this->assertEquals('/foo.css', $this->extension->prefetch('/foo.css', array('as' => 'style', 'crossorigin' => true)));
80+
81+
$link = (new Link('prefetch', '/foo.css'))->withAttribute('as', 'style')->withAttribute('crossorigin', true);
82+
$this->assertEquals(array($link), array_values($this->request->attributes->get('_links')->getLinks()));
83+
}
84+
85+
public function testPrerender()
86+
{
87+
$this->assertEquals('/foo.css', $this->extension->prerender('/foo.css', array('as' => 'style', 'crossorigin' => true)));
88+
89+
$link = (new Link('prerender', '/foo.css'))->withAttribute('as', 'style')->withAttribute('crossorigin', true);
90+
$this->assertEquals(array($link), array_values($this->request->attributes->get('_links')->getLinks()));
91+
}
92+
}

src/Symfony/Bridge/Twig/composer.json

Expand all lines: src/Symfony/Bridge/Twig/composer.json
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"twig/twig": "~1.28|~2.0"
2121
},
2222
"require-dev": {
23+
"fig/link-util": "^1.0",
2324
"symfony/asset": "~2.8|~3.0",
2425
"symfony/finder": "~2.8|~3.0",
2526
"symfony/form": "^3.2.5",
@@ -34,7 +35,8 @@
3435
"symfony/stopwatch": "~2.8|~3.0",
3536
"symfony/console": "~2.8|~3.0",
3637
"symfony/var-dumper": "~2.8.10|~3.1.4|~3.2",
37-
"symfony/expression-language": "~2.8|~3.0"
38+
"symfony/expression-language": "~2.8|~3.0",
39+
"symfony/web-link": "~3.3"
3840
},
3941
"suggest": {
4042
"symfony/finder": "",
@@ -48,7 +50,8 @@
4850
"symfony/security": "For using the SecurityExtension",
4951
"symfony/stopwatch": "For using the StopwatchExtension",
5052
"symfony/var-dumper": "For using the DumpExtension",
51-
"symfony/expression-language": "For using the ExpressionExtension"
53+
"symfony/expression-language": "For using the ExpressionExtension",
54+
"symfony/web-link": "For using the WebLinkExtension"
5255
},
5356
"autoload": {
5457
"psr-4": { "Symfony\\Bridge\\Twig\\": "" },

0 commit comments

Comments
 (0)
0