Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

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
Prev Previous commit
Next Next commit
Use a request attribute instead of a stateful service
  • Loading branch information
dunglas committed Apr 10, 2017
commit aba1dbb11848aca4e7c9fd815b4919d8996b263d
26 changes: 16 additions & 10 deletions 26 src/Symfony/Bridge/Twig/Extension/WebLinkExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@

namespace Symfony\Bridge\Twig\Extension;

use Fig\Link\GenericLinkProvider;
use Fig\Link\Link;
use Symfony\Component\WebLink\WebLinkManagerInterface;
use Symfony\Component\HttpFoundation\RequestStack;

/**
* Twig extension for the Symfony WebLink component.
Expand All @@ -21,11 +22,11 @@
*/
class WebLinkExtension extends \Twig_Extension
{
private $manager;
private $requestStack;

public function __construct(WebLinkManagerInterface $manager)
public function __construct(RequestStack $requestStack)
{
$this->manager = $manager;
$this->requestStack = $requestStack;
}

/**
Expand Down Expand Up @@ -54,12 +55,17 @@ public function getFunctions()
*/
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);
}

$this->manager->add($link);
$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;
}
Expand All @@ -74,7 +80,7 @@ public function link($uri, $rel, array $attributes = array())
*/
public function preload($uri, array $attributes = array())
{
return $this->link($uri, WebLinkManagerInterface::REL_PRELOAD, $attributes);
return $this->link($uri, 'preload', $attributes);
}

/**
Expand All @@ -87,7 +93,7 @@ public function preload($uri, array $attributes = array())
*/
public function dnsPrefetch($uri, array $attributes = array())
{
return $this->link($uri, WebLinkManagerInterface::REL_DNS_PREFETCH, $attributes);
return $this->link($uri, 'dns-prefetch', $attributes);
}

/**
Expand All @@ -100,7 +106,7 @@ public function dnsPrefetch($uri, array $attributes = array())
*/
public function preconnect($uri, array $attributes = array())
{
return $this->link($uri, WebLinkManagerInterface::REL_PRECONNECT, $attributes);
return $this->link($uri, 'preconnect', $attributes);
}

/**
Expand All @@ -113,7 +119,7 @@ public function preconnect($uri, array $attributes = array())
*/
public function prefetch($uri, array $attributes = array())
{
return $this->link($uri, WebLinkManagerInterface::REL_PREFETCH, $attributes);
return $this->link($uri, 'prefetch', $attributes);
}

/**
Expand All @@ -126,7 +132,7 @@ public function prefetch($uri, array $attributes = array())
*/
public function prerender($uri, array $attributes = array())
{
return $this->link($uri, WebLinkManagerInterface::REL_PRERENDER, $attributes);
return $this->link($uri, 'prerender', $attributes);
}
Copy link
Member

Choose a reason for hiding this comment

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

We don't need this method.


/**
Expand Down
29 changes: 17 additions & 12 deletions 29 src/Symfony/Bridge/Twig/Tests/Extension/WebLinkExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,18 @@
use Fig\Link\Link;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\Twig\Extension\WebLinkExtension;
use Symfony\Component\WebLink\WebLinkManager;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;

/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class WebLinkExtensionTest extends TestCase
{
/**
* @var WebLinkManager
* @var Request
*/
private $manager;
private $request;

/**
* @var WebLinkExtension
Expand All @@ -33,60 +34,64 @@ class WebLinkExtensionTest extends TestCase

protected function setUp()
{
$this->manager = new WebLinkManager();
$this->extension = new WebLinkExtension($this->manager);
$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->manager->getLinkProvider()->getLinks()));
$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->manager->getLinkProvider()->getLinks()));
$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->manager->getLinkProvider()->getLinks()));
$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->manager->getLinkProvider()->getLinks()));
$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->manager->getLinkProvider()->getLinks()));
$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->manager->getLinkProvider()->getLinks()));
$this->assertEquals(array($link), array_values($this->request->attributes->get('_links')->getLinks()));
}

public function testGetName()
{
$this->assertEquals('web_link', (new WebLinkExtension(new WebLinkManager()))->getName());
$this->assertEquals('web_link', $this->extension->getName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,7 @@

<services>

<service id="web_link.manager" class="Symfony\Component\WebLink\WebLinkManager" public="false">
<argument type="service">
<service class="Fig\Link\GenericLinkProvider" />
</argument>
</service>
<service id="Symfony\Component\WebLink\WebLinkManagerInterface" alias="web_link.manager" public="false" />

<service id="web_link.add_link_header_listener" class="Symfony\Component\Link\WebLink\AddLinkHeaderListener" public="false">
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like there is a typo in class name here, should be: Symfony\Component\WebLink\AddLinkHeaderListener

<argument type="service" id="web_link.manager" />

<tag name="kernel.event_subscriber" />
</service>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ public function testAssetsDefaultVersionStrategyAsService()
public function testWebLink()
{
$container = $this->createContainerFromFile('web_link');
$this->assertTrue($container->hasDefinition('web_link.manager'));
$this->assertTrue($container->hasDefinition('web_link.add_link_header_listener'));
}

public function testTranslator()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public function load(array $configs, ContainerBuilder $container)
if (interface_exists(WebLinkManagerInterface::class)) {
$definition = $container->register('twig.extension.weblink', WebLinkExtension::class);
$definition->setPublic(false);
$definition->addArgument(new Reference('web_link.manager'));
$definition->addArgument(new Reference('request_stack'));
}

foreach ($configs as $key => $config) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@

namespace Symfony\Component\WebLink\EventListener;

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;
use Symfony\Component\WebLink\WebLinkManagerInterface;

/**
* Adds the Link HTTP header to the response.
Expand All @@ -26,12 +26,10 @@
*/
class AddLinkHeaderListener implements EventSubscriberInterface
{
private $manager;
private $serializer;

public function __construct(WebLinkManagerInterface $manager)
public function __construct()
{
$this->manager = $manager;
$this->serializer = new HttpHeaderSerializer();
Copy link
Member

Choose a reason for hiding this comment

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

having this class be instantiated directly here means it's an internal implementation detail to the listener
imho, we should either make it a service, or merge the implement in this class, don't you think?

Copy link
Member Author

@dunglas dunglas Apr 7, 2017

Choose a reason for hiding this comment

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

It's indeed an internal detail of the listener.

But the HttpHeaderSerializer class can be used alone through the component. I don't think it makes sense (for now) to expose it as a service but it may be useful for library creators to serialize a list of Link instances to a valid HTTP header value (it's almost all this component do).

I would keep this as is (btw the serializer is final but not internal for the same reason).

}

Expand All @@ -41,12 +39,12 @@ public function onKernelResponse(FilterResponseEvent $event)
return;
}

if ($value = $this->serializer->serialize($this->manager->getLinkProvider())) {
$event->getResponse()->headers->set('Link', $value, false);

// Free memory
$this->manager->clear();
$linkProvider = $event->getRequest()->attributes->get('_links');
if (!$linkProvider instanceof LinkProviderInterface || !($links = $linkProvider->getLinks())) {
Copy link
Member

Choose a reason for hiding this comment

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

no need for the brackets after the ||

return;
}

$event->getResponse()->headers->set('Link', $this->serializer->serialize($links), false);
}

/**
Expand Down
8 changes: 4 additions & 4 deletions 8 src/Symfony/Component/WebLink/HttpHeaderSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

namespace Symfony\Component\WebLink;

use Psr\Link\LinkProviderInterface;
use Psr\Link\LinkInterface;

/**
* Serializes a list of Link instances to a HTTP Link header.
Expand All @@ -25,14 +25,14 @@ final class HttpHeaderSerializer
/**
* Builds the value of the "Link" HTTP header.
*
* @param LinkProviderInterface $linkProvider
* @param LinkInterface[]|\Traversable $links
*
* @return string|null
*/
public function serialize(LinkProviderInterface $linkProvider)
public function serialize($links)
{
$elements = array();
foreach ($linkProvider->getLinks() as $link) {
foreach ($links as $link) {
if ($link->isTemplated()) {
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
use Fig\Link\GenericLinkProvider;
use Fig\Link\Link;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\WebLink\EventListener\AddLinkHeaderListener;
use Symfony\Component\WebLink\WebLinkManager;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
Expand All @@ -28,14 +28,14 @@ class AddLinkHeaderListenerTest extends TestCase
{
public function testOnKernelResponse()
{
$manager = new WebLinkManager(new GenericLinkProvider());
$manager->add(new Link('preload', '/foo'));

$subscriber = new AddLinkHeaderListener($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);
Expand All @@ -48,7 +48,6 @@ public function testOnKernelResponse()
);

$this->assertEquals($expected, $response->headers->get('Link', null, false));
$this->assertEmpty($manager->getLinkProvider()->getLinks());
}

public function testSubscribedEvents()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,19 @@ protected function setUp()

public function testSerialize()
{
$linkProvider = new GenericLinkProvider(array(
$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($linkProvider));
$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(new GenericLinkProvider()));
$this->assertNull($this->serializer->serialize(array()));
}
}
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.