Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Link Util Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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:

  1. src/Link.php — constructor is new Link($href, array $rels = [], array $attributes = []).
  2. src/LinkProvider.php — a collection with getLinksByRel($rel), getLinks(), addLink(), withLink(), withoutLink().
  3. src/EvolvableLink.php / EvolvableLinkProvider.php — immutable copies returned from with* methods.
  4. The four traits (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.

Implementation Patterns

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.

Gotchas and Tips

  • No __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.
  • Rels are not normalized or validated. 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.
  • Escaping is your job. Quotes, backslashes, commas, and semicolons in href or attribute values will corrupt the header. Percent-encode hrefs (rawurlencode on query values) and addcslashes($v, '"\\') on attribute values.
  • Laravel header arrays. $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.
  • Frozen since 2016. No return types, no 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.
  • Interface, not class, in your signatures. Type-hint 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].
  • Traits are the underused feature. If you have a 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.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core