The WebLink component manages the Link HTTP header defined by Web Linking
(RFC 8288). Most applications use it to preload and prefetch assets, but links
are also how HTTP APIs tell clients where related resources are. Symfony 8.2
adds the two specifications that build on Web Linking and were missing from
the component: the Link-Template header (RFC 9652) and link sets (RFC 9264).
Sending Templated Links
The Link header can only carry concrete URLs. When the target isn't known in
advance (e.g. the URL of any item of a collection) you describe it with a
URI template such as /users/{id}. The Link class already supported
templated links, but there was no header to send them in, so Symfony silently
dropped them from the response.
In Symfony 8.2, links added to the response are split between two headers:
those pointing to a concrete URL go to Link and those using a URI template
go to the new Link-Template header:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
// src/Controller/UserController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\WebLink\Link;
class UserController extends AbstractController
{
#[Route('/users', name: 'user_index')]
public function index(Request $request): Response
{
$this->addLink($request, new Link('preload', '/app.css')->withAttribute('as', 'style'));
$this->addLink($request, new Link('item', '/users/{id}'));
// the response includes these headers:
// Link: </app.css>; rel="preload"; as="style"
// Link-Template: "/users/{id}"; rel="item"
return $this->render('user/index.html.twig');
}
}
You don't have to change anything in your application: the existing Link
headers stay the same.
Reading Link-Template Headers
When consuming third-party APIs, use the new LinkTemplateHeaderParser class
to turn the header back into Link objects:
1 2 3 4 5 6 7 8 9
use Symfony\Component\WebLink\LinkTemplateHeaderParser;
// $response is the HttpClient response of the request made to the API
$headers = $response->getHeaders()['link-template'] ?? [];
$links = new LinkTemplateHeaderParser()->parse($headers)->getLinks();
$links[0]->getHref(); // '/users/{id}'
$links[0]->isTemplated(); // true
$links[0]->getRels(); // ['item']
Publishing Link Sets
Headers are not always the right place for links: there may be too many of them,
or you may want to publish them without making clients request each resource.
A link set is a collection of links published as a standalone document.
For example, RFC 9727 uses link sets to list the APIs of a host at the
/.well-known/api-catalog URL.
RFC 9264 defines two formats for these documents. The application/linkset
format uses the same syntax as the Link header, so the existing serializer
and parser already handle it. For the application/linkset+json format,
inject the new JsonLinksetSerializer class in your services and controllers:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
// src/Controller/ApiCatalogController.php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\WebLink\JsonLinksetSerializer;
use Symfony\Component\WebLink\Link;
class ApiCatalogController
{
#[Route('/.well-known/api-catalog', name: 'api_catalog')]
public function __invoke(JsonLinksetSerializer $serializer): Response
{
$links = [
new Link(Link::REL_SERVICE_DESC, 'https://example.com/api/books/openapi.yaml')
->withAttribute('anchor', 'https://example.com/api/books')
->withAttribute('type', 'application/yaml'),
new Link(Link::REL_SERVICE_DOC, 'https://example.com/docs/books')
->withAttribute('anchor', 'https://example.com/api/books')
->withAttribute('type', 'text/html'),
];
// the second argument is an optional bitmask of json_encode() options
$document = $serializer->serialize($links, \JSON_UNESCAPED_SLASHES);
return new Response($document, headers: [
'Content-Type' => 'application/linkset+json; profile="https://www.rfc-editor.org/info/rfc9727"',
]);
}
}
In the resulting document (formatted here for readability), links are grouped by
link context (their anchor attribute) and then by relation type:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
{
"linkset": [
{
"anchor": "https://example.com/api/books",
"service-desc": [
{
"href": "https://example.com/api/books/openapi.yaml",
"type": "application/yaml"
}
],
"service-doc": [
{
"href": "https://example.com/docs/books",
"type": "text/html"
}
]
}
]
}
Reading and Advertising Link Sets
Use the new JsonLinksetParser class to read link set documents:
1 2 3 4 5 6 7 8
use Symfony\Component\WebLink\JsonLinksetParser;
$links = new JsonLinksetParser()->parse($document)->getLinks();
$links[0]->getHref(); // 'https://example.com/api/books/openapi.yaml'
$links[0]->getRels(); // ['service-desc']
// ['anchor' => 'https://example.com/api/books', 'type' => 'application/yaml']
$links[0]->getAttributes();
Finally, to tell clients that a resource has a link set, add a link with the
linkset relation type to its response:
1 2
$this->addLink($request, new Link(Link::REL_LINKSET, '/books/1234/links')
->withAttribute('type', 'application/linkset+json'));