markup-carve/symfony-carve
Symfony bundle for Carve, a modern extensible markup language. Parse and render Carve documents in your apps with service container integration, Twig support, configurable options, and cached compilation for fast, flexible markup processing.
What changed in 0.1.5. This is a scoped feature release on a pre-1.0 package. Three things matter: (1) opt-in file-backed rendering via an absolute include_root plus CarveRenderer::renderFile() / renderFileWithReport(); (2) a report object that exposes resolved and attempted include targets; (3) dependency movement — carve-php bumped to ^0.1.9 and psr/log added as a runtime dependency for an optional warning logger.
Architecture fit. The valuable seam for us is CarveRenderer, which is plain PHP and framework-agnostic. The package's Twig filters are Symfony-oriented and are not a fit for a default Laravel stack (Blade, no Twig). So our architecture posture should be: consume the renderer as a library, ignore the Twig/Symfony integration surface. That is exactly the surface the release notes say is unchanged — render() and the filters still take anonymous strings and never touch the filesystem. In other words, the release adds capability without widening the part of the package we'd actually couple to.
Integration feasibility. Low friction. Composer-installable, small API, and psr/log is already present transitively in any Laravel app via Monolog, so the new runtime dependency is effectively free. The carve-php ^0.1.9 floor is the only real constraint — it forces a coordinated bump, and it is itself a pre-1.0 dependency, which compounds churn risk.
Where the new feature actually pays off. Before 0.1.5, file-backed rendering meant we would file_get_contents() ourselves and then call render(), which left include resolution entirely outside the library. Now the library owns resolution and returns a report of resolved and attempted targets. That report is the interesting artifact: it gives us a deterministic, content-derived signal to key a cache on, including the "attempted but missing" set. A cache invalidated by "the set of files this render actually depended on" is materially better than one keyed on a single root template mtime. The companion change — warnings to the logger omitting resolver details — is a small but real hardening win, since absolute filesystem paths stop leaking into log aggregation.
Technical risk. Three items dominate:
include_root must be absolute and the resolver now has filesystem authority. Whether includes are constrained to the root (no ../ escape, no absolute-path injection from template content) is unverified from the notes and is the single most important thing to confirm before this touches user-supplied content.composer update drift.Key questions.
include_root, and does it reject absolute paths originating from template content?renderFile() throw, or does it degrade with a warning? Does renderFileWithReport() surface missing targets in the report without throwing?CarveRenderer stateless and safe under Laravel Octane / long-lived workers (no memoized resolver state leaking between requests)?Stack fit. Treat this as a rendering library, not a framework integration. Bind a single CarveRenderer instance as a singleton in a service provider, configured with a fixed absolute include_root pointing at a dedicated templates/partials directory — never base_path(), never the application root, never a user-writable path. Expose one narrow application-facing service (e.g. CarveContentRenderer) with two methods — render-string and render-file — so that no controller or job ever touches CarveRenderer directly. That adapter is where we absorb pre-1.0 churn.
Migration path. There is no prior assessment and no existing integration, so this is a greenfield adoption rather than a migration. The only "migration" is from whatever ad-hoc string templating exists today; the 0.1.5 notes confirm that the anonymous-string render() path is unchanged, so a string-rendering spike can be done in isolation with zero filesystem exposure before any file-backed work begins. Sequence the file-backed capability strictly after the string path is proven.
Compatibility. psr/log is satisfied by Monolog in any standard Laravel app — verify with composer why psr/log rather than assuming, since it is declared runtime-optional and the version constraint is unstated in the notes. The carve-php ^0.1.9 floor must be checked against whatever is currently locked; if another package pins a lower carve-php, we have a conflict that forces a resolution decision at adoption time. Laravel version compatibility is a non-issue for the renderer core but is untested for the Symfony/Twig side, which we are not using.
Sequencing.
../, absolute paths, symlinks, null bytes) before enabling include_root anywhere. This gates step 3.renderFileWithReport() from day one, even though we do not yet cache, so we accumulate real reports and learn the include graph shape.warning; confirm empirically that no absolute paths appear in output.Maintenance. The package is small, single-maintainer, and pre-1.0, with a dependency that is also pre-1.0. Budget for deliberate, tested upgrades rather than passive drift: exact version pins in composer.json, a composer.lock that is committed and reviewed, and a short upgrade checklist that includes re-running the adversarial include tests. The 0.1.4 → 0.1.5 change is a good calibration point — it moved a dependency floor in a patch release, which is the kind of thing that breaks a build without warning.
Support. We own the integration surface. There is no vendor SLA and no realistic expectation of one at this stage. The adapter pattern is the support strategy: if the upstream API shifts, the blast radius is one class. Escalation for upstream defects is a GitHub issue, so plan for the possibility of carrying a local patch — which argues for keeping our usage free of the Twig/Symfony surface, since that is the part most likely to diverge from our needs.
Scaling. Rendering is CPU-bound string processing with per-render filesystem reads once includes are enabled, no built-in cache, and no built-in include memoization apparent from the notes. Two consequences: (a) the app-level cache keyed on the report is not optional at volume — it is the scaling mechanism; (b) hot paths should render from strings or from a warm cache rather than repeatedly resolving a deep include tree. Watch for a per-render resolver that stats the same partials on every request. Statelessness under Octane is an open question (see above) and must be settled before we deploy on a long-lived worker runtime.
Failure modes.
composer update pulling a new carve-php minor that changes include semantics. Mitigated by pins.Ramp-up. Low. The public surface is a handful of methods, and the Twig filters — the only conceptually fiddly part — are out of scope for us. A mid-level Laravel engineer should be productive within a day using render() and renderFile(), and roughly another day on the report-driven cache key. The real ramp-up cost is not the API; it is the path-containment review and the upgrade discipline, both of which need an owner before adoption rather than after.
How can I help you explore Laravel packages today?