Skip to content

Add a new Link component #22273

New issue

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

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

Already on GitHub? Sign in to your account

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
"require": {
"php": ">=5.5.9",
"doctrine/common": "~2.4",
"fig/link-util": "^1.0",
Copy link
Member

Choose a reason for hiding this comment

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

this dep is in require-dev in all components, but in require here, looks suspicious

Copy link
Member Author

Choose a reason for hiding this comment

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

We need this package in the Twig extension and the FrameworkBundle (even at runtime). But the those packages have only a soft dependency to WebLink. In the context of the full stack, I think it makes sense to add it to have something working out of the box.

Copy link
Contributor

Choose a reason for hiding this comment

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

It's not required by any of the packages in replace, so it should indeed remain in require-dev.

Copy link
Member Author

Choose a reason for hiding this comment

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

It is required if you use WebLink && (Twig Bridge || Framework Bundle). We cannot express such conditions with Composer, but the full stack framework install WebLink + Twig Bridge + Framework Bundle so it needs to have this package as dependency.

Copy link
Member

Choose a reason for hiding this comment

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

Still looks inconsistent to me. symfony/symfony should not behave differently than using standalone components...

Copy link
Member Author

Choose a reason for hiding this comment

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

fig/link-util added back as a dependency of the WebLink component. It fixes the inconsistency, makes the component easier to use for end users and... it's the only one implementation of PSR-13 for now.

"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",
Expand Down Expand Up @@ -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",
Expand Down
26 changes: 1 addition & 25 deletions src/Symfony/Bridge/Twig/Extension/AssetExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -22,12 +21,10 @@
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;
}

/**
Expand All @@ -38,7 +35,6 @@ public function getFunctions()
return array(
new \Twig_SimpleFunction('asset', array($this, 'getAssetUrl')),
new \Twig_SimpleFunction('asset_version', array($this, 'getAssetVersion')),
new \Twig_SimpleFunction('preload', array($this, 'preload')),
);
}

Expand Down Expand Up @@ -71,26 +67,6 @@ public function getAssetVersion($path, $packageName = null)
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.
*
Expand Down
137 changes: 137 additions & 0 deletions src/Symfony/Bridge/Twig/Extension/WebLinkExtension.php
Original file line number Diff line number Diff line change
@@ -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());
Copy link
Member Author

Choose a reason for hiding this comment

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

Is this ok or should we always populate this key? The current implementation is more memory efficient.

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, I really think the ideal case would be for Symfony\Component\HttpFoundation\Request to implement Psr\Link\EvolvableLinkProviderInterface. That's what I understand after reading PSR-13.

But I understand that's not probably not possible considering the mutable nature of our Request. That said, I think using attributes for this is wrong.

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually, I really think the ideal case would be for Symfony\Component\HttpFoundation\Request to implement Psr\Link\EvolvableLinkProviderInterface.

I agree but as you pointed out, it's not possible.

That said, I think using attributes for this is wrong.

Why?

$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);
}
}
45 changes: 0 additions & 45 deletions src/Symfony/Bridge/Twig/Tests/Extension/AssetExtensionTest.php

This file was deleted.

92 changes: 92 additions & 0 deletions src/Symfony/Bridge/Twig/Tests/Extension/WebLinkExtensionTest.php
Original file line number Diff line number Diff line change
@@ -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()));
}
}
7 changes: 5 additions & 2 deletions src/Symfony/Bridge/Twig/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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": "",
Expand All @@ -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\\": "" },
Expand Down
Loading