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

Markdown Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

  • Install with composer require dflydev/markdown. Expect an "abandoned package" warning from Composer 2 — that's accurate, but the package still installs.
  • Look at exactly two files first: 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.
  • First use case — rendering a post body from a database column:
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.

  • Before you build anything on top of it, sanity-check your PHP version. Render one document containing a table, a fenced code block, a footnote, and a raw <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.

Implementation Patterns

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.

Gotchas and Tips

  • It is deprecated and abandoned. No fixes for PHP 8.x, no security response. The realistic exit is 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.
  • Output is XHTML, not HTML5. <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.
  • Test link titles. [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.
  • The parser is stateful. 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.
  • Footnote and header ids are globally scoped per parse. Rendering five documents and concatenating them gives you duplicate fn:1 / heading ids. Render each independently and prefix ids in a post-pass if they land on one page.
  • No caching. The regex pipeline re-runs in full on every call. Large documents are noticeably slow. Cache at the app layer — persisted body_html or Cache::remember — and move on.
  • Config typos fail silently. The constructor just reads array keys; headerIdFunc instead of header_id_func produces no error at all. Write one test per knob you depend on.
  • Debugging weird output: bisect. Extra's "markdown inside HTML blocks" feature means a stray blank line inside a <div> flips parsing modes and changes the result. Keep a scratch route that runs transformMarkdown() on the smallest failing snippet.
  • Autoloading is PSR-0 under the dflydev\markdown namespace. Composer handles it, but on case-sensitive filesystems with --classmap-authoritative, verify the classes resolve once before shipping.
  • Nothing you'd expect from a Laravel package exists here — no config publishing, no facade, no Markdown rule for validation. Everything (directive, caching, sanitizing, TOC) is yours to build in that one wrapper class.
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