fig/link-util
A small PSR-13 implementation for PHP: interfaces and classes for representing links (URI, rel, attributes) and link providers, plus serializers for HTTP Link headers and HAL-style JSON. Integrates with PSR-7 without extra dependencies.
fig/link-util is a ~200-line reference implementation of PSR-13 (psr/link). It gives you value objects for HTTP Link headers — Fig\Link\Link, Fig\Link\EvolvableLink, Fig\Link\LinkProvider, Fig\Link\EvolvableLinkProvider, plus traits if you want to implement the interfaces on your own classes. It does not parse or serialize headers; it only models them.
composer require fig/link-util
Look at these files first, in order — the whole library is smaller than most of your controllers:
src/Link.php — constructor is new Link($href, array $rels = [], array $attributes = []).src/LinkProvider.php — a collection with getLinksByRel($rel), getLinks(), addLink(), withLink(), withoutLink().src/EvolvableLink.php / EvolvableLinkProvider.php — immutable copies returned from with* methods.LinkTrait, EvolvableLinkTrait, LinkProviderTrait, EvolvableLinkProviderTrait) — read these if you plan to implement PSR-13 on your own domain classes.First use case: emit an RFC 8288 Link header on a paginated API response so clients can follow next/prev/first/last without parsing the JSON body.
use Fig\Link\Link;
use Fig\Link\LinkProvider;
$links = new LinkProvider();
$links->addLink(new Link($paginator->url(1), ['first']));
$links->addLink(new Link($paginator->nextPageUrl(), ['next']));
$links->addLink(new Link($paginator->previousPageUrl(), ['prev']));
return response()->json($data)->header(
'Link',
implode(', ', array_map(fn ($l) => sprintf('<%s>; rel="%s"', $l->getHref(), implode(' ', $l->getRels())), $links->getLinks()))
);
The point of using the package instead of string-concatenating is that links become objects you can pass around, filter by rel, and — if you later switch to a PSR-7 stack — hand to libraries that accept LinkInterface.
Pagination header via an API Resource. Laravel's LengthAwarePaginator already gives you url(), nextPageUrl(), previousPageUrl(), lastPage(). Wrap those in Link objects inside a ResourceCollection or a middleware, then serialize once. This keeps the URL construction in the paginator and the header shape in one place.
Global preload / preconnect middleware. A middleware that inspects the response and a per-route config is a clean home for preload, preconnect, canonical, and alternate links. Use EvolvableLink here so that a later middleware can add a preload rel without rebuilding the link:
public function handle($request, Closure $next)
{
$response = $next($request);
$links = new EvolvableLinkProvider();
$links = $links->withLink(
(new EvolvableLink('/build/app.css'))->withRel('preload')->withAttribute('as', 'style')
);
return $response->header('Link', $this->serialize($links));
}
HATEOAS in JSON:API / HAL responses. Instead of embedding ad-hoc _links maps, build a LinkProvider per resource and let one serializer produce both the header and the embedded links object. This guarantees the header and body never drift.
Rel-based filtering when you don't know what's on the response. getLinksByRel('next') returns a LinkInterface[] — useful when a downstream handler wants "the next link if there is one" without re-parsing a header string. Note there is no getLinkByRel; you get an array and take [0].
PSR-7 bridge. In a standard Laravel app this is overkill. If you are using nyholm/psr7 or laminas/laminas-diactoros (e.g. behind Octane, a queue consumer for an upstream API, or a GraphQL gateway), accept LinkInterface in your own service signatures so you can swap the implementation; the concrete Fig\Link\Link is fine as the default.
Serializer helper. You will write this once. Put it in app/Support/Http/LinkHeader.php:
public static function serialize(LinkProviderInterface $provider): string
{
return implode(', ', array_map(function (LinkInterface $link) {
$parts = [sprintf('<%s>', $link->getHref())];
foreach ($link->getRels() as $rel) {
$parts[] = sprintf('rel="%s"', $rel);
}
foreach ($link->getAttributes() as $key => $value) {
$parts[] = sprintf('%s="%s"', $key, addcslashes($value, '"\\'));
}
return implode('; ', $parts);
}, $provider->getLinks()));
}
Skip links with a null href (nextPageUrl() returns null on the last page) before constructing the Link — the constructor will accept null and you get an <>; rel="next" header, which is wrong.
__toString(), no serializer, no parser. The package models links only. Every example above requires your own sprintf loop. Budget for that up front; it's a 20-line class, but surprises people who assume PSR packages ship serializers.EvolvableLink is immutable. ->withRel('next') returns a new object; forgetting to reassign is the #1 bug. LinkProvider (non-evolvable) is mutable and has addLink() — don't mix the two mental models in one class.new Link('/x', ['Next']) and ['next'] produce different objects; getLinksByRel('next') will not find Next. RFC 8288 wants lowercase; lowercase on input if you care.isTemplated() is a flag, not an expander. It just returns the boolean you passed as the fourth constructor argument. There is no RFC 6570 URI-template expansion in this package — pair it with guzzlehttp/uri-template if you actually need that.href or attribute values will corrupt the header. Percent-encode hrefs (rawurlencode on query values) and addcslashes($v, '"\\') on attribute values.$response->header('Link', $arrayOfStrings) emits multiple Link header lines; implode(', ', ...) emits one. Both are legal per RFC 8288, but some clients and proxies handle one form better — pick one and be consistent across the app.null hrefs. nextPageUrl() / previousPageUrl() are null at the boundaries. Filter them out; don't rely on the constructor to reject them.strict_types, no declare, no typed properties. It works fine on PHP 8.1+ because the surface is tiny, but static analysis (PHPStan level 6+) and IDE autocompletion will be weaker than you're used to. If you only need Link + LinkProvider, copying the ~200 LOC into app/Support and adding types is a legitimate option — but you then lose the "another package understands this" benefit, so only do it if you're not passing these objects to third-party code.Psr\Link\LinkInterface / LinkProviderInterface in your services so a future swap (custom class implementing PSR-13, or a PSR-7 lib's own implementation) doesn't ripple through the codebase.LinksByRel returns an array, possibly empty, never null. Always guard if ($links->getLinksByRel('next')) before indexing [0].Page or ApiResource domain object that should be a link provider, use LinkProviderTrait; and implement the interface — you avoid a wrapper class and keep one less allocation per response.How can I help you explore Laravel packages today?