Skip to content

Runtime coupling of generated code

What the generated code does now: self-contained, no generator at runtime

Section titled “What the generated code does now: self-contained, no generator at runtime”

The generated Data classes stand alone in your repo. When a class needs a rule or transformer Laravel does not ship, the generator inlines that support class into your own Support namespace alongside the Data classes:

  • App\Data\Support\HostnameRule (a format: hostname property)
  • App\Data\Support\MultipleOfRule (a multipleOf property)
  • App\Data\Support\Rfc3339DateTimeRule (a format: date-time property)
  • App\Data\Support\Rfc3339TimeRule (a format: time property)
  • App\Data\Support\Iso8601DurationRule (a format: duration property)
  • App\Data\Support\MapObjectTransformer (every additionalProperties map property, via #[WithTransformer(...)])
  • App\Data\Support\NoUnknownPropertiesRule (the default additionalProperties: false enforcement)
  • App\Data\Support\RespondsWithStatus (the route middleware that enforces non-200 declared success status codes)

(The App\Data prefix mirrors whatever Data namespace you configured: the support namespace is always the Data namespace plus a \Support suffix.)

These classes are part of the committed generated output (they are pinned by the conformance golden test), emitted into <output>/Support/ and drift-checked byte-for-byte like every Data class. Only the support classes a spec actually references are emitted: a spec with no multipleOf carries no MultipleOfRule, and NoUnknownPropertiesRule appears whenever a schema declares additionalProperties: false (enforced by default, suppressed only under --no-enforce-closed-objects). They exist because Laravel has no native rule for multipleOf, no strict RFC3339 date-time/time rule, no ISO 8601 duration rule, no hostname rule, no way to reject undeclared properties on a closed object, and no way to force an empty map to serialize as {} rather than []. The generator supplies those small classes and inlines the referenced ones into your code.

The result is the headline this decision delivers: codewithagents/openapi-laravel is no longer a runtime dependency of consuming applications, it is a pure dev-time tool. A class that uses multipleOf or a date-time format loads MultipleOfRule from your own App\Data\Support namespace, not from the generator package. The historical coupling described below is the problem this shipped change removed.

Why the old coupling sat in tension with the philosophy

Section titled “Why the old coupling sat in tension with the philosophy”

The project’s stated philosophy is you own the output: readable PHP in your repo that keeps working even if you stop using the generator. The runtime coupling (now removed) dented that in two ways:

  • It was not fully owned. Eight classes that your generated rules() depended on lived in vendor/, outside the code you committed and review in diffs.
  • A composer update could change runtime behavior under committed code. Because the support classes were versioned with the generator, upgrading the generator could change how your already-generated, already-committed classes validate and serialize, without you regenerating anything. That was the exact silent-change surface the versioning policy pins down. Inlining closes it: a rule changes only when you regenerate and review the diff.

spatie/laravel-data itself is a genuine runtime dependency under every option below, that is unavoidable and expected, the generated classes are laravel-data classes. The decision below concerned only the eight openapi-laravel-owned support classes, which now live in your own Support namespace.

This had to settle before 1.0.0, because the import lines are part of the frozen output format. Moving the namespace later (for example from CodeWithAgents\OpenApiLaravel\Support\... to App\Data\Support\...) is a breaking output change, exactly what the 1.0.0 freeze is meant to prevent. The discriminator-aware cast (#38) has since shipped using spatie’s native PropertyMorphableData (an abstract morphable base plus morph()), so it added no new runtime support class. The inlined set under Option B is therefore the eight classes above (including NoUnknownPropertiesRule, emitted whenever a closed object is present, which is now the default), not a growing list driven by #38.

Option A: keep the runtime dependency (status quo)

Section titled “Option A: keep the runtime dependency (status quo)”

Generated code keeps importing the support classes from the generator package. codewithagents/openapi-laravel stays a require (not require-dev) for consumers.

Pros

  • Zero duplication. One canonical implementation of each rule, maintained and tested in one place.
  • Bug fixes flow to consumers without regeneration. If Rfc3339DateTimeRule has a parsing bug, a composer update fixes it everywhere.
  • Simplest generator. No extra files to emit, no namespace rewriting, no per-project copies to keep in sync.
  • Already shipped and proven (the e2e demo runs exactly this).

Cons

  • Generated code is not self-contained. “Stop using the generator and your code still works” is false: remove the package and the classes that import Support\... break.
  • Silent runtime-behavior changes on upgrade, as above. This is the strongest argument against A.
  • The generator is a heavier dependency than it needs to be. Consumers pull the whole generator (parser, emitter, symfony/yaml) into production just to get eight small runtime classes.

Option B: inline the support classes into the consumer’s output (adopted and shipped)

Section titled “Option B: inline the support classes into the consumer’s output (adopted and shipped)”

The generator emits the referenced support classes into the consumer’s own namespace alongside the Data classes (for example App\Data\Support\MultipleOfRule), and the generated code imports from there. They are owned, committed, and drift-checked like everything else. This is the adopted choice, and it has shipped.

Pros

  • Fully owned output. The philosophy holds literally: every class the generated code touches is in your repo and survives removing the generator.
  • No runtime dependency on the generator. It becomes a pure dev-time / require-dev tool.
  • No silent runtime changes. A rule only changes when you regenerate and review the diff, the same contract as the rest of the output.

Cons

  • Duplication across projects. Every consuming project carries its own copy of the support classes. A fix to a rule reaches them only on regeneration (which is consistent with the owned-output model, but means no automatic propagation).
  • More generated files (the support classes emitted per project) and a small amount of generator complexity (emitting them, namespacing them, drift-checking them).
  • The drift check now covers the support classes too, which is arguably more correct but is more surface.

Split the eight support classes into a separate, minimal, semver-frozen package (for example codewithagents/openapi-laravel-runtime) that changes essentially never. The generator depends on it; generated code imports from it; consumers require only the tiny runtime, not the whole generator.

Pros

  • Runtime footprint shrinks to the support classes plus laravel-data, no parser or emitter in production.
  • One canonical implementation (no duplication), like Option A.
  • A frozen package has a clean, narrow versioning story: it changes almost never, so the silent-upgrade risk is small and explicitly bounded.

Cons

  • Still a runtime dependency, so the “owned output, survives removing the generator” promise is only partly satisfied (you still need the runtime package).
  • A second package to publish, version, and maintain, plus the coordination of keeping the generator’s emitted imports in lockstep with the runtime package’s namespace.
  • Splitting a package for a handful of small classes can feel heavier than the problem warrants.
A: keep dependencyB: inline into outputC: frozen runtime package
Generated code self-containedNoYesNo
Runtime dep on generatorYes (full)NoneTiny runtime only
Duplication across projectsNonePer-project copiesNone
Silent runtime change on composer updatePossibleNoneRare (frozen)
spatie/laravel-data still requiredYesYesYes
Generator/maintenance complexityLowestMedium (emit + drift-check)Medium (second package)
Aligns with “you own the output”PartlyFullyPartly

Option B (inline into the consumer’s output) is adopted. It is the only option that makes the you own the output promise literally true, it removes the silent-runtime-change surface entirely (rule behavior changes only on a reviewed regeneration, like everything else), and it makes the generator a clean dev-time tool. The cost, the support classes emitted per project plus a little emitter work, is small and exactly the kind of owned, drift-checked output the project already produces. The duplication tradeoff is consistent with the model: you already re-own every Data class per project, the support classes are no different.

Option C was the reasonable fallback if duplication had been judged unacceptable: it keeps one canonical implementation while shrinking the runtime footprint and bounding the upgrade risk to a near-frozen package. Option A is the lowest-effort path but is the weakest fit for the stated philosophy and carries the silent-upgrade surface that the versioning policy otherwise closes.