kirschbaum-development/paragon
Paragon, by Kirschbaum Development, is a lightweight Laravel package that adds a clean, expressive API for common PHP application needs, helping you keep code organized, consistent, and easy to maintain across projects.
Install as a dev dependency, not a runtime one. Paragon is a generator; nothing should reference it at runtime.
composer require --dev kirschbaum-development/paragon:^1.2.1
php artisan vendor:publish --tag=paragon-config # confirm the tag with `php artisan vendor:publish --list`
Pin ^1.2.1 or later. Do not start on ^1.2.0 — v1.2.1 is the escaping fix, and starting on the older constraint means your first regeneration is a security patch you have to retrofit.
Where to look first, in order:
config/paragon.php (or whatever the published filename is) — this is the whole integration surface. It defines which PHP enum classes are read (source paths or explicit class list) and where the .ts files land. Get this right before touching anything else.php artisan list | grep -i paragon — confirm the exact Artisan command name and available flags. Do not assume it matches the package name.First use case — one enum, end to end. Pick a single backed enum you already duplicate by hand, ideally a small one with an awkward value (a status with a hyphen, or anything containing a quote or slash — you want to exercise the escaping path on day one, not six months in):
enum OrderStatus: string
{
case Pending = 'pending';
case Shipped = 'shipped';
}
Point the config at app/Enums, generate, and commit the resulting .ts alongside a one-line import in your frontend. Success criterion is tsc --noEmit passing against the generated file and your app build consuming it — not "the Artisan command exited 0."
Verify the capability before you commit. The package metadata circulating for this project is inconsistent: the summary describes Eloquent-backed application settings, while the release history describes PHP-enum-to-TypeScript generation. Everything below assumes the codegen capability, which the release notes support. Spend the first ten minutes reading the actual README in vendor/kirschbaum-development/paragon and confirming which one you installed. If it is the settings package, stop — this assessment does not apply.
Commit the generated artifacts. The alternative — generating on every frontend build — sounds cleaner and is worse: contract changes vanish into build output instead of appearing as a reviewable diff, and a broken generator takes down the frontend build rather than failing an explicit step. Committed files make vocabulary changes visible in code review, which is the entire point. Mark them in .gitattributes:
resources/js/types/enums/** linguist-generated=true
Add a CI drift gate on day one. This is the single highest-value pattern in the package:
php artisan <paragon-generate>
git diff --exit-code -- resources/js/types/enums/
Without this, generated files become a second source of truth and a second source of drift — strictly worse than hand-maintained constants, because now nobody knows which side is stale. With it, the build fails the moment a PHP enum changes without regeneration.
Migrate by domain, one PR at a time. Pilot on one representative enum (with a special character), then batch the rest by area — OrderStatus + PaymentState in one PR, roles and permissions in another. Reviewers can reason about a single vocabulary; they cannot reason about a forty-file mechanical diff. If a batch PR contains edits to hand-written frontend code, that's a signal the generated types don't fit your conventions and the config needs adjusting, not the call sites.
Derive UI affordances from the enum instead of restating them. The durable win is not OrderStatus.Pending — it's that your status dropdown, your badge colour map, and your filter chips all key off the same generated type, so Record<OrderStatus, string> becomes an exhaustive-checked map. TypeScript will then fail the build when someone adds a case, which is exactly the defect class you're buying.
Use it as the contract for API-shaped types, not just values. Typed props in Inertia/Vue/React, request payload unions, query-param validators — anywhere you'd otherwise write type Status = 'pending' | 'shipped' by hand. If your enum is backed, you get the wire values; if it's pure, you get the case names. Make sure the team agrees on which one lands in the generated file before the first batch, because changing the convention later rewrites every consumer.
Acceptance criteria, per PR: tsc clean against regenerated output, frontend build green, drift gate green. Anything else is incomplete.
Escaping is fixed but not proven complete. v1.2.1 escapes string values and method outputs, closing a malformed-output and injection-style risk when values contain quotes or backslashes. That's a hardening patch, not a proof. Before you trust this on values influenced by config or tenant data, build a fixture enum containing ", ', `, \, a newline, and a literal ${} in both a case value and a method return, generate, and confirm tsc compiles it. Keep that fixture in the repo permanently — it is your regression guard against a future patch quietly undoing the fix.
Broad globs will publish enums you didn't intend to expose. A config path of app/ or app/** will sweep in internal enums that aren't part of your public vocabulary and will now be baked into the frontend bundle forever. Enumerate deliberately: either an explicit app/Enums directory with a team convention that public-facing enums live there and nowhere else, or an explicit class list. The directory convention is easier to enforce in review.
Determinism across machines is the silent diff-noise failure. Confirm that output is byte-identical on macOS, Linux and CI. The usual suspects: enum case ordering (source order vs alphabetical), trailing newline handling, CRLF vs LF, and — if the generator evaluates enum methods — any method whose return depends on config, locale, env(), or the current time. Never generate from a method whose output isn't a pure function of source code; if you must, freeze it with a test that runs generation in CI and diff-checks.
Method evaluation is the sharp edge. If methods are rendered into the TypeScript, understand when they run. Generation-time evaluation means the value is frozen at build; any method that reads config(), now(), or a database row will produce different output in CI than on a laptop. Treat those as unsupported and keep generated methods to pure lookups (label(), color()) returning scalars.
Output location must be inside your frontend's compiled graph. Generated files that sit outside tsconfig include/paths will compile in isolation and then fail on import. Decide the path once (something like resources/js/types/enums/) and align both the package config and the tsconfig alias in the same PR. Similarly, check lint/format tooling — Prettier rewriting generated files will make your drift gate fail on every run; add the directory to .prettierignore (or let the generator format) and pick one owner for whitespace.
Naming collisions and reserved words. Two enums with the same short name in different PHP namespaces will collide in a flat output directory; TypeScript reserved words (default, class, enum itself) as enum names will fail to compile. Mirror PHP namespaces into directories, or enforce globally unique enum names, before the second batch — retrofitting a naming scheme after fifty files are imported is a bad afternoon.
Debugging procedure. Command produces nothing → check the configured source paths resolve and that the enum classes are autoloadable (composer dump-autoload); command produces files but imports fail → check tsconfig include and path aliases; drift gate fails immediately after a green local run → almost always line endings or formatter interference; generated TypeScript is malformed → you're on ^1.2.0 or have a value shape the escaper doesn't cover, and the fixture above will tell you which.
Extension points, if they exist, are the right place for house style. If the package exposes publishable stubs or templates, override them for indentation, header comments ("generated — do not edit"), and const-object-with-as const style instead of native enum (native enum has known interop friction with isolatedModules, Babel, and verbatimModuleSyntax). Publish the stub into your repo rather than patching vendor/, so the choice survives composer update.
Know the exit. At 8 stars and zero dependents this is an unproven dependency. That's acceptable precisely because it's MIT-licensed and emits plain text — worst case, someone forks the generator or replaces it with thirty lines of nikic/php-parser over the same directory. Keep the source-path config narrow and the output directory isolated so that exit stays a one-week job rather than an archaeology project.
How can I help you explore Laravel packages today?