jbzoo/mermaid-php
jbzoo/mermaid-php lets PHP applications generate Mermaid diagram definitions with a clean, fluent API. Build flowcharts, sequence diagrams, class diagrams, and more, then render them in docs, dashboards, or web UIs without hand-writing Mermaid syntax.
Install and verify
composer require jbzoo/mermaid-php --dev
Check composer.json for the PHP constraint (jbzoo packages are typically PHP 8.1+ with strict_types and no Laravel coupling), then confirm the package's binary is exposed:
ls vendor/bin | grep -i mermaid
Where to look first, in order
README.md — the canonical list of supported diagram types and the fluent/array builder API. This is the source of truth for method names.tests/ — jbzoo packages ship heavy PHPUnit suites; the tests are effectively the API documentation for edge cases (escaping, options, CLI flags).src/ — look for the diagram builder classes (flowchart/sequence/class/ER/Gantt) and whether raw-script output (__toString()) is separated from rendering (mmdc/Node).vendor/bin/mermaid --help — if a CLI ships, it is the fastest way to test syntax without touching app code.First use case (15 minutes)
Generate a deployment/checkout flowchart into a docs file, don't render yet:
use JBZoo\Mermaid\Mermaid; // verify namespace in src/
$chart = Mermaid::flowchart()
->direction('LR')
->node('start', 'Checkout')
->node('pay', 'Charge card')
->node('ok', 'Order paid')
->edge('start', 'pay')
->edge('pay', 'ok', 'success');
file_put_contents(base_path('docs/checkout.mmd'), (string) $chart);
Prove the source string is valid by pasting it into the Mermaid Live Editor once. Only after that wire up server-side rendering — rendering is the part that drags in Node, Chromium and CI concerns, so keep it out of your first loop.
1. One class per diagram, checked into the repo
Treat diagrams like migrations: a small class returning a diagram object, a generated .mmd committed alongside, and the render as a build artifact.
final class CheckoutDiagram
{
public function build(): mixed
{
return Mermaid::flowchart()
->direction('LR')
// ...
;
}
}
This gives you code review on diagram changes, git diff on the source, and a single place to update when a flow changes.
2. Artisan command + drift check in CI
class GenerateDiagramsCommand extends Command
{
protected $signature = 'docs:diagrams';
public function handle(): int
{
foreach (config('diagrams') as $name => $class) {
file_put_contents(
base_path("docs/diagrams/{$name}.mmd"),
(string) app($class)->build()
);
}
return self::SUCCESS;
}
}
Then in CI: php artisan docs:diagrams && git diff --exit-code docs/diagrams — fails the build when code changed but diagrams didn't. Cheap, and it catches the common failure mode of stale architecture docs.
3. ER diagram straight from the Laravel schema
The highest-value day-to-day use: derive erDiagram from Schema::getTables() / getColumns() (native in Laravel 11+, doctrine/dbal on older versions). Loop tables → emit entities, loop columns → attributes, loop foreign keys → relations. Run it on a schedule and commit the result; it stays accurate for free and replaces hand-drawn schema images in onboarding docs.
Similarly: generate stateDiagram-v2 from a state machine config, sequenceDiagram from your HTTP kernel/event-listener map, classDiagram from selected domain classes.
4. Split rendering strategies — don't force Node everywhere
<pre class="mermaid"> block and let mermaid.js render in the browser. Zero server dependencies, works offline in the repo. Use a Blade component so the CDN/version pin lives in one file.5. Cache renders by content hash
If you render server-side inside any automated flow, key the output file on sha1($script . $rendererVersion . $theme) and skip the subprocess on hit. This turns a multi-diagram docs build from ~30s into ~1s on the common path.
6. Test the script, not the SVG
$this->assertStringContainsString('pay -->|success| ok', (string) $diagram);
SVG output embeds generated IDs, timestamps and renderer-version-dependent markup — snapshotting it produces permanently flaky tests. Snapshot the .mmd string (normalize \r\n → \n) and leave the renderer to a single smoke test.
7. Integration tip: escape at the boundary, not in the builder
If labels come from user data (table names, workflow step names, ticket titles), wrap them in one escapeLabel() helper that quotes the label and neutralizes ", (, ), ;, #, <, > and newlines. Call it wherever external strings enter, never scatter escaping through diagram classes.
Syntax and escaping
;, and #. Always emit A["Charge card (Stripe)"] — quoted — and be aware " inside a quoted label needs entity-encoding (#quot;) in many mermaid versions.%% is a comment; %%{init: {"theme":"dark"}}%% is a directive. Directives must be on the first line of the script or they are silently ignored in several mermaid versions. Same for flowchart vs graph — be consistent, they parse slightly differently.<br/>, not \n. A stray real newline inside a node label is the #1 cause of "Parse error".Security
securityLevel: 'strict' in mermaid.initialize(). Do not skip this if labels come from a database.Renderer / environment
@mermaid-js/mermaid-cli → Puppeteer → headless Chromium. In Docker/CI you'll need --no-sandbox (usually exposed as a puppeteer config file), plus the Chromium shared libs (libnss3, libatk, libgbm, fonts). Missing fonts → tofu boxes in PNG output; install fonts-dejavu or fonts-noto.mermaid-cli explicitly and pin the browser-side mermaid.js version to match; bump both together.Diagram quality
direction deliberately: LR for pipelines, TD for hierarchies. Default TD on a wide flow produces a very tall image that's unreadable in a README.%% generated by docs:diagrams — do not edit) so nobody hand-edits the .mmd and then loses it to the next regeneration.Browser-side rendering quirks
id; duplicated IDs happen when rendering the same component twice and cause one diagram to vanish.mermaid.initialize({startOnLoad: true}) only runs once. With Livewire, Turbo, htmx or Alpine-driven DOM swaps you must re-invoke mermaid.run({nodes: [...]}) after the swap; otherwise newly inserted diagrams stay as raw text.max-width: 100%; height: auto in CSS or it will overflow on mobile.Extension points
__toString() for raw output; use that as the universal export and keep rendering pluggable behind a small interface (render(string $script): string) so you can swap mmdc for an HTTP call to a render service, or a fake in tests.theme: 'base' + themeVariables) in one factory so all diagrams stay visually consistent, and so a re-theme is a one-file change.How can I help you explore Laravel packages today?