dflydev/markdown
dflydev/markdown is a PHP Markdown parser that converts Markdown and Markdown Extra text into HTML. It supports standard Markdown plus extras like tables, footnotes, and definition lists, making it easy to render user-authored content in PHP applications.
composer require dflydev/markdown. Expect an "abandoned package" warning from Composer 2 — that's accurate, but the package still installs.src/dflydev/markdown/MarkdownParser.php and MarkdownExtraParser.php. There is no service provider, no config file, no facade, no Artisan command. It's a plain library with an IMarkdownParser interface (transform() / transformMarkdown()) and a constructor that takes a config array.use dflydev\markdown\MarkdownExtraParser;
$parser = new MarkdownExtraParser();
$html = $parser->transformMarkdown($post->body);
Use MarkdownExtraParser unless you specifically need vanilla Markdown behavior. Extra gives you tables, fenced code blocks, footnotes, definition lists, and {#id} header attributes — the things people actually expect from "markdown" in 2024.
<div>, then look for notices or malformed output. This package's last meaningful release predates PHP 7; if it's going to break on your runtime, find out on day one, not after you've written the Blade directive.Wrap it once and bind it in the container. Never new MarkdownParser() in a controller or a view.
namespace App\Support;
use dflydev\markdown\MarkdownExtraParser;
class MarkdownRenderer
{
private MarkdownExtraParser $parser;
public function __construct()
{
$this->parser = new MarkdownExtraParser([
'header_id_func' => fn ($text) => \Str::slug($text),
'url_filter_func' => fn ($url) => $this->safeUrl($url),
]);
}
public function render(string $markdown): string
{
return $this->parser->transformMarkdown($markdown);
}
}
Register it as a singleton in a service provider. That class is your single migration point when you eventually swap the parser.
Blade directive.
Blade::directive('markdown', fn ($expr) => "<?php echo app(\App\Support\MarkdownRenderer::class)->render($expr); ?>");
Then @markdown($post->body). This deliberately bypasses Blade escaping — see the gotchas.
Render once, store the HTML. The standard pattern is a body_html column filled on save:
static::saving(function (Post $post) {
$post->body_html = app(MarkdownRenderer::class)->render($post->body);
});
Keep the raw markdown column too. You will re-render everything when you migrate parsers, and you can't do that from HTML.
Config knobs that matter in an app:
header_id_func — stable anchor ids for a docs/blog table of contents.url_filter_func — reject javascript: and data: schemes on user-submitted content.no_markup => true — for comments or other content where you don't want raw HTML passed through. It strips inline HTML rather than escaping it; check the output on real content.tab_width — affects nested-list indentation. Leave at 4 unless you know your editors emit something else.code_block_content_func / code_span_content_func — wrap code in <pre><code class="language-php"> for highlighting without post-processing the whole document.Table of contents. There's no TOC extraction. Either build it from the rendered HTML (preg_match_all on <h[1-6] id="...">) or run a heading regex over the markdown before rendering. The first is less error-prone.
Testing. Snapshot tests are the only practical approach: a fixtures directory of input.md / expected.html pairs driven by a PHPUnit dataset. Pin expected.html byte-for-byte — whitespace and <br /> placement are part of the contract, and you'll want that frozen before you migrate.
michelf/php-markdown — this package is a port of it, the class names are nearly identical (Michelf\MarkdownExtra, transform()), and the constructor config keys largely line up. If you want something that keeps receiving maintenance, league/commonmark (CommonMark-compliant) or erusev/parsedown are the alternatives, but both need a real migration test suite, not a mechanical swap.<br />, <hr />, <img />. If a linter or AMP pipeline downstream expects void tags, you'll be post-processing.{{ $html }} will double-escape. Markdown output is already HTML; use {!! !!} or the directive. That also means you own the XSS risk: raw HTML in the source passes through unless no_markup is set, and the default URL handling is not a sanitizer. For user-supplied content, run the rendered HTML through HTMLPurifier or configure no_markup and url_filter_func. Markdown is not a security boundary.[x](http://example.com "onmouseover=...") — titles go through the parser's own entity table, not a full escaper. Verify behavior if you accept untrusted markdown.transformMarkdown() populates internal arrays ($this->urls, $this->titles, footnotes, headers). It resets per call, so a reused instance is fine under PHP-FPM, but don't share one across concurrent coroutines under Swoole/RoadRunner. Instantiate per request on async runtimes.fn:1 / heading ids. Render each independently and prefix ids in a post-pass if they land on one page.body_html or Cache::remember — and move on.headerIdFunc instead of header_id_func produces no error at all. Write one test per knob you depend on.<div> flips parsing modes and changes the result. Keep a scratch route that runs transformMarkdown() on the smallest failing snippet.dflydev\markdown namespace. Composer handles it, but on case-sensitive filesystems with --classmap-authoritative, verify the classes resolve once before shipping.How can I help you explore Laravel packages today?