Circular Dependencies
The generator analyzes DTO relationships before rendering classes and throws early when it finds an eager dependency cycle.
This is a generation-time check, not a runtime hydration check.
When Cycle Detection Runs
Cycle detection happens during definition building, before DTO files are written.
That means a cycle fails generation even if you have not instantiated the DTOs yet.
What Counts as a Dependency
The dependency analyzer currently looks at:
- DTO field types such as
User - array and collection element types such as
User[] - nullable and namespaced types such as
?User[]or\App\Dto\UserDto - union and intersection members such as
Foo|Bar,Foo&Bar, and parenthesized DNF shapes - collection
singularType - explicit
dtofield metadata - DTO inheritance via
extends
What Breaks a Cycle
Lazy fields are excluded from the dependency graph:
Dto::create('User')->fields(
Field::dto('manager', 'User')->asLazy(),
)This lets you model recursive graphs without blocking generation.
Important
Nullable fields alone do not currently remove a dependency from the analyzer. If you need to break a generation-time cycle, use lazy.
Basic Example
Eager Cycle That Fails
return Schema::create()
->dto(Dto::create('User')->fields(
Field::dto('team', 'Team'),
))
->dto(Dto::create('Team')->fields(
Field::dto('owner', 'User'),
))
->toArray();Generation fails because User -> Team -> User is an eager cycle.
Lazy Edge That Passes
return Schema::create()
->dto(Dto::create('User')->fields(
Field::dto('team', 'Team')->asLazy(),
))
->dto(Dto::create('Team')->fields(
Field::dto('owner', 'User'),
))
->toArray();This works because the lazy team field is skipped during cycle analysis.
Collections and Advanced Types
Cycle detection also applies to:
- collections using
singularType - unions such as
User|Team - intersections such as
Foo&Bar - parenthesized DNF shapes such as
(Foo|Bar)&Baz
If any eager branch introduces a cycle, generation fails. Making the field lazy skips the whole field from the analyzer.
Self-References
Direct self-references do not count as a cycle in the analyzer:
Dto::create('Category')->fields(
Field::dto('parent', 'Category'),
)This is allowed by the generator. In practice, recursive structures are still usually better modeled as lazy fields when large subtrees are involved.
Inheritance
extends relationships are part of the dependency graph. A DTO that extends another DTO depends on that parent during analysis.
Troubleshooting
When generation fails, the exception message shows the discovered cycle path.
Typical fixes:
- Mark one edge in the cycle as lazy.
- Reconsider whether two DTOs need to reference each other directly.
- Move one side of the relationship to an identifier field instead of a nested DTO.
Related Guides
- Config Builder for
asLazy() - Performance for lazy hydration tradeoffs
- Troubleshooting for generator error messages