Skip to content

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 dto field metadata
  • DTO inheritance via extends

What Breaks a Cycle ​

Lazy fields are excluded from the dependency graph:

php
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 ​

php
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 ​

php
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:

php
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:

  1. Mark one edge in the cycle as lazy.
  2. Reconsider whether two DTOs need to reference each other directly.
  3. Move one side of the relationship to an identifier field instead of a nested DTO.

Released under the MIT License.