iamcal/lib_autolink
iamcal/lib_autolink is a lightweight PHP library that automatically converts plain-text URLs and email addresses into HTML anchor tags. It provides a simple API for autolinking user-generated content such as comments, messages, and forum posts.
Understand what you're getting first. iamcal/lib_autolink is a single ~200-line PHP file (autolink.inc.php) exposing one static method. There is no Laravel service provider, no facade, no composer.json, and no namespace. This means "installation" is a manual step, not a composer require — plan accordingly.
1. Vendor the file and classmap it.
mkdir -p app/Support/lib_autolink
# copy autolink.inc.php from the repo into that folder
"autoload": {
"classmap": ["app/Support/lib_autolink/autolink.inc.php"]
}
composer dump-autoload
Using repositories: [{type: "path"}] will not work without a composer.json in the package. Classmap or a plain require_once in a service provider are your two realistic options.
2. Smoke test it in tinker before wiring anything up.
lib_autolink::autolink('see http://example.com for details');
// see <a href="http://example.com">http://example.com</a>
3. Read the source before you trust it. The entire public surface is autolink($text, $options = []) plus the $options array merged at the top of the method — attribute injection (target, rel) and link-text truncation are your only knobs. There is no documented escaping policy, so you must determine for yourself whether the surrounding text should be HTML-escaped before or after you hand it over.
First use case: a comments.body or tickets.description column stored as plain text and rendered in Blade. You want bare http://… strings to become clickable links without adopting a full Markdown pipeline.
Wrap it once, then never touch the class again. Calling lib_autolink::autolink() directly from views or controllers means you have no single place to enforce escaping order, attributes, or truncation.
namespace App\Support;
class Autolink
{
public static function render(?string $text, array $options = []): string
{
if ($text === null || trim($text) === '') {
return '';
}
// Escape FIRST so surrounding text can never emit raw HTML,
// then let the library add anchors to the escaped string.
$escaped = e($text);
return lib_autolink::autolink($escaped, $options + [
'attributes' => [
'rel' => 'nofollow noopener',
'target' => '_blank',
],
]);
}
}
The $options + [...] merge (rather than array_merge) lets callers override defaults without you writing a merge helper.
Blade directive, so templates stay readable:
// AppServiceProvider::boot()
Blade::directive('autolink', fn ($expression) =>
"<?php echo \\App\\Support\\Autolink::render({$expression}); ?>"
);
<p>@autolink($comment->body)</p>
Note it returns HTML — @autolink(...) is correct; wrapping the result in {{ }} is not.
Model accessor for feed-style models:
protected function bodyHtml(): Attribute
{
return Attribute::get(fn () => Autolink::render($this->body));
}
Keep it out of $appends unless you actually need it in JSON — it's a regex pass per access, cheap but not free.
Config-driven policy. Put target, rel, and truncation length in config/autolink.php and merge them in the wrapper, so web and email render paths can diverge deliberately (e.g. no target attribute inside email templates, where _blank is meaningless and nofollow matters less).
Split plain-text from Markdown paths. If some columns are Markdown and some are plain text, run league/commonmark (with its autolink extension) on the Markdown ones and lib_autolink only on the plain-text ones. Never chain them: Markdown output is HTML, and passing HTML into this library is a gotcha waiting to happen (see below).
Test the edges with a Pest dataset, not just the happy path:
it('autolinks', function (string $in, string $expected) {
expect(\App\Support\Autolink::render($in))->toBe($expected);
})->with([
['go to http://example.com.', 'go to <a href="http://example.com">http://example.com</a>.'],
['(see https://example.com/a_(b))', '(see <a href="https://example.com/a_(b)">https://example.com/a_(b)</a>)'],
['mail me [email protected]', 'mail me <a href="mailto:[email protected]">[email protected]</a>'],
]);
Add cases for www. prefixes, ftp://, existing <a> markup, Unicode/IDN domains, and a javascript: payload.
Escape order is the whole security story. The library emits <a> tags but does not own your escaping policy. If you pass raw user input straight in and the surrounding text contains <script>, you've just shipped an XSS hole. Always e() first. Verify separately that the library itself escapes href and anchor text and does not permit javascript: or data: schemes — do not assume.
Do not feed it HTML. If a field already contains anchors or markup, this library may double-link, nest anchors, or mangle the existing tags. It expects plain text. Route Markdown/HTML content through a real parser, not this.
Handle the null return. preg_* functions return null on backtracking-limit failure. A pathological input (huge unbroken strings, many stacked URLs) can silently blank the rendered output. Guard in the wrapper: fall back to the escaped text rather than returning an empty string.
$out = lib_autolink::autolink($escaped, $options);
return $out !== null ? $out : nl2br($escaped);
Watch for PHP 8.x deprecations. This is PHP 5-era code with a 2015 last release. Run it on your deployed PHP versions in CI (8.1–8.3), not just locally, with error_reporting set to strict. If you see deprecation noise it'll become an error the day someone tightens error handling.
Trailing punctuation is handled, parentheses are dicey. The library strips common trailing punctuation (., ,, )), but balanced-paren URLs like https://en.wikipedia.org/wiki/Foo_(bar) are the classic failure mode — the closing ) gets eaten. Test it; if it matters for your content, don't rely on the library alone.
Truncation and target are attribute-level, not text-level. Confirm the exact option keys against the source in your vendored copy — the options array is undocumented and varies. Truncation shortens the visible link text (with an ellipsis), not the href, so long URLs stay clickable but readable.
Pin and own it. With a decade-old last release and zero dependents, treat the vendored file as your code. Add a comment at the top of the copied file noting the upstream commit hash and date, name an owner in the PR description, and add a review date (e.g. in a docs/dependencies.md). When you eventually outgrow it, the wrapper makes the swap a one-line binding change.
Performance for bulk rendering. For per-request rendering of a handful of fields it's negligible. For backfills, exports, email digests, or API responses over thousands of records, benchmark first and consider caching the rendered HTML where the source is immutable — the rendered output is deterministic given the same options.
Escape the href, not just the text — test it. Write one Pest test that submits <script>alert(1)</script>http://x.com and asserts the output contains no unescaped <script>, and another that submits javascript:alert(1) and asserts no href="javascript:. If either fails, stop and pick a maintained alternative (league/commonmark's autolink extension or s9e/text-formatter) before shipping.
Extension points are your wrapper, not the library. There is nothing to hook, extend, or subclass. If you need scheme allowlisting, custom rel policies per surface, or link previews later, you're building those in App\Support\Autolink — and that's exactly why it should be the only class in your app that knows the library exists.
How can I help you explore Laravel packages today?