awcodes/html-faker
A Laravel package that adds HTML-aware fake data generators to Faker, letting you seed tests and demos with realistic markup—headings, paragraphs, lists, links, and more—instead of hand-written placeholder HTML.
Architecture fit. awcodes/html-faker is a FakerPHP provider — a few hundred lines that plug into the Faker generator Laravel already ships. That is the entire architecture. It has no service layer, no config surface, no persistence, and no runtime footprint outside the code path that calls it. For our purposes this is close to ideal: a dev-only fixture utility that sits beside fakerphp/faker and disappears from production entirely. It fits our stack the same way fake() does, because it is fake() with HTML formatters attached.
The scope is narrow in a way that matters for a product decision. It generates plausible markup strings — headings, paragraphs, lists, links. It does not parse, validate, sanitize, or reason about DOM structure. Any roadmap assumption that we can lean on it for content integrity, accessibility checking, or sanitizer testing is wrong, and should be corrected before it reaches a plan.
Integration feasibility. Low effort, low ambiguity. composer require --dev awcodes/html-faker:^0.4, auto-discovery registers the provider, one factory state or seeder call site produces output. The realistic spike is half a day: install, generate one sample on a branch, dd() the raw string, confirm structured markup rather than escaped or truncated text. If that check passes, integration is done.
The real feasibility question is not technical. It is whether we are willing to own a package that will never receive another commit. The repository is archived as of the last release (2025-06-10, v0.4.0). There is no Laravel 13 path, no PHP 8.5 path, no security triage, no issue response. Stars (11) and dependents (0) mean there is no ecosystem evidence and no informal support channel. Adoption is permanent ownership by default, and the only honest posture is "frozen, with a documented fork contingency."
Technical risk. Three risks, all manageable, none zero:
tests/Support when the pin fails.Key questions to resolve before committing:
src/? The repo is pre-1.0 and archived; README and code are the same age. Run it once before anyone writes against it.composer why-not awcodes/html-faker ^0.4 against our actual lockfile.require-dev breaks that deploy — check the deploy process before committing to --dev.Stack fit. Strong, with a caveat. Every Laravel app with the default require-dev already pulls fakerphp/faker, so this package composes with an existing dependency rather than adding a new tree. It lives entirely in factory states, seeders, and test fixtures — the exact places our team already reaches for fake(). No new concepts, no new mental model, no onboarding tax.
The recommended path is borrow, not buy. For a utility of this scope, the defensible default is to lift the provider into tests/Support (or app/Support if seeders live in the app) with MIT attribution, rather than carry a frozen dependency we do not control. Three reasons: we keep the code, we drop the supply-chain and abandonment risk entirely, and we gain the ability to extend toward domain-specific blocks — product cards, pricing tables, testimonial modules — that a generic HTML generator will never produce. If the team prefers the dependency route for speed, that is defensible only behind a single seam with a named owner and a documented exit.
The single-seam wrapper is non-negotiable either way. Never call the package from more than one place:
namespace Tests\Support;
class HtmlFixture
{
/** @return string Raw, unsanitized HTML. Test/demo data only. */
public function blocks(int $count = 4): string { /* delegate */ }
public function paragraph(): string { /* ... */ }
public function inline(): string { /* ... */ }
}
This is the pattern that converts a dependency risk into a maintenance cost. Half a day of work buys the entire migration, whenever it comes.
Migration path and sequencing.
require-dev, generate one sample on a branch, dd() the raw string. Confirm structured markup. Confirm the PHP and illuminate/support constraints against our lockfile.HtmlFixture as the sole call site. Add the profile-based extension surface inside the wrapper (profile('email'), profile('article')) rather than forking the package.body longText column — Article, Post, or CMS Block — and add a withHtmlBody() factory state. That is the entire adoption. Everything after is variations on it./dev/html-preview route that renders generated markup through real Blade views and the real sanitizer pipeline. This is the highest-leverage use case: it surfaces prose-style breakage on nested lists and long links before anyone files a "layout bug" that is actually fixture output.Compatibility. Laravel 11 and 12 only as declared. Pre-1.0, archived, no forward path. Treat the declared matrix as permanent.
Sequencing caution. Register any Faker provider additions in require-dev-only code paths. Registering a dev fixture provider in AppServiceProvider that boots in production is the classic way a test utility becomes a production dependency.
Maintenance. The package itself requires no maintenance because it will never change. The ownership burden is real and should be assigned explicitly. A named owner, a documented fork contingency, and an inventory entry flagged as archived are the minimum. Budget 1–2 engineer-days, once, for the day the pin breaks and the provider moves into our own support code.
The larger maintenance cost is cultural. The most probable day-one failure is a test that asserts on exact generated strings — it will read as flaky rather than obviously misused, and it will consume debugging time disproportionate to its cause. Assert on structure (toContain('<p>')) or seed the generator; never snapshot output into a fixture file, which reintroduces the hand-maintained fixture the package was meant to eliminate.
Support. None available. No issue triage, no security response, no maintainer. That is an accepted consequence of adopting an archived repo, not a surprise to be discovered later. The compensating control is that the code is a few hundred lines and readable in ten minutes, so internal support is genuinely achievable — provided someone has read it.
Scaling. Generation cost is not the bottleneck in a bulk seeder; row-by-row insertion is. Chunked inserts via Model::insert(Model::factory()->count(200)->withHtmlBody()->raw()) are the right shape. Keep samples modest — a handful of blocks per row — because deeply nested markup across hundreds of rows is measurable generation time and pointless in a local database. Column types should be text/longText; storing generated markup as JSON for "structured fixtures" gives the worst of both worlds and loses direct renderability.
Failure modes to plan for:
dd() the raw string before it hits Blade. Nine times out of ten it is generator output — an unclosed tag, an empty heading — not a CSS or browser bug. Five-second check, saves an hour.fake() seeding leaks across the suite. fake()->seed() is global state. Reseeding for deterministic HTML also fixes every Faker-generated attribute after that point in the same process, including tests nobody touched. Prefer structure assertions; if determinism is required, seed once in setUp and accept that the rest of the test data is now fixed.{!! !!} — that is the point — but it must be local-gated at the route level, not visually marked. A copy of that Blade file reaching a real page is a genuine injection surface.require-dev. If a seeder referencing the wrapper runs during production or staging deploy, moving the package to --dev breaks that deploy. Safe default: require-dev plus a written rule that production seeders must not depend on HTML-faker output.Ramp-up. Essentially zero for anyone who has written a Faker seeder — the mental model transfers directly, and the wrapper means most engineers never touch the package API at all. The onboarding that does matter is the governance framing: what this tool is for (ephemeral, internal, disposable test data) versus what it is not for (curated, stable, sometimes customer-visible demo content). Those have different requirements, different budgets, and often different owners. Making that distinction explicit during ramp-up is worth more than the package itself, because it becomes the standing rule for every future test utility the team considers pulling in.
Strategic read. This is engineering enablement, not a product capability. It justifies a small, time-boxed investment in fixture and seeder tooling for QA velocity. It should never appear as a roadmap line item or be cited as a customer-facing feature in planning materials. The strongest artifact to come out of evaluating it may not be the adoption at all — it may be the explicit answer to "what bar must a dev-only dependency clear?" For an 11-star, zero-dependent, archived, pre-1.0 utility, that answer is worth more than the code.
How can I help you explore Laravel packages today?