Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ return Schema::create()
Field::string('content'),
Field::dto('author', 'Author')->required(),
Field::collection('tags', 'Tag')->singular('tag'),
Field::bool('published')->defaultValue(false),
Field::bool('published')->default(false),
Field::string('publishedAt'),
))
->dto(Dto::immutable('Author')->fields(
Expand Down Expand Up @@ -201,8 +201,11 @@ Full documentation available at **[php-collective.github.io/dto](https://php-col

- [Getting Started](https://php-collective.github.io/dto/guide/) - Quick start guide with examples
- [Configuration Builder](https://php-collective.github.io/dto/guide/config-builder) - Fluent API for defining DTOs
- [Runtime API](https://php-collective.github.io/dto/guide/runtime-api) - Core DTO methods and global runtime options
- [Examples](https://php-collective.github.io/dto/guide/examples) - Practical usage patterns
- [Circular Dependencies](https://php-collective.github.io/dto/guide/circular-dependencies) - How generation-time cycle detection works
- [TypeScript Generation](https://php-collective.github.io/dto/reference/typescript) - Generate TypeScript interfaces
- [JSON Schema Generation](https://php-collective.github.io/dto/reference/jsonschema) - Generate JSON Schema for DTO definitions
- [Schema Importer](https://php-collective.github.io/dto/reference/importer) - Bootstrap DTOs from JSON data/schema
- [Performance](https://php-collective.github.io/dto/guide/performance) - Benchmarks and optimization tips

Expand Down
3 changes: 3 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export default defineConfig({
text: 'Configuration',
items: [
{ text: 'Config Builder', link: '/guide/config-builder' },
{ text: 'Runtime API', link: '/guide/runtime-api' },
{ text: 'Validation', link: '/guide/validation' },
{ text: 'Advanced Types', link: '/guide/advanced-types' },
{ text: 'Shaped Arrays', link: '/guide/shaped-arrays' },
Expand All @@ -65,6 +66,7 @@ export default defineConfig({
text: 'Advanced',
items: [
{ text: 'Advanced Patterns', link: '/guide/advanced-patterns' },
{ text: 'Circular Dependencies', link: '/guide/circular-dependencies' },
{ text: 'Framework Integration', link: '/guide/framework-integration' },
{ text: 'Separating Generated Code', link: '/guide/separating-generated-code' },
{ text: 'Design Decisions', link: '/guide/design-decisions' },
Expand All @@ -86,6 +88,7 @@ export default defineConfig({
items: [
{ text: 'CLI Reference', link: '/reference/cli' },
{ text: 'TypeScript Generation', link: '/reference/typescript' },
{ text: 'JSON Schema Generation', link: '/reference/jsonschema' },
{ text: 'Schema Importer', link: '/reference/importer' },
]
}
Expand Down
118 changes: 118 additions & 0 deletions docs/guide/circular-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
title: Circular Dependencies
---

# 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.

## Related Guides

- [Config Builder](./config-builder) for `asLazy()`
- [Performance](./performance) for lazy hydration tradeoffs
- [Troubleshooting](./troubleshooting) for generator error messages
9 changes: 6 additions & 3 deletions docs/guide/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,7 @@ $profile = new ProfileDto(['bio' => null, 'followers' => null]); // OK

// Non-null values are validated
$profile = new ProfileDto(['bio' => str_repeat('x', 501)]);
// Exception: Field 'bio' must be at most 500 characters
// Exception: Validation failed in App\Dto\ProfileDto: bio must be at most 500 characters
```

### Extracting Validation Rules
Expand All @@ -667,8 +667,11 @@ $rules = $dto->validationRules();
// 'age' => ['min' => 0, 'max' => 150],
// ]

// Use with framework validators
$validator = new FrameworkValidator($rules);
// Map to framework-native validation rules or constraints
$frameworkRules = [
'username' => ['required', 'string', 'min:' . $rules['username']['minLength']],
'email' => ['required', 'email'],
];
```

## Lazy Loading
Expand Down
45 changes: 35 additions & 10 deletions docs/guide/framework-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ title: Framework Integration

This library is **framework-agnostic** by design. It works with any PHP framework without requiring wrapper packages, while still offering deep integration possibilities.

Do We Need Wrapper Libraries?
## Do We Need Wrapper Libraries?

| Feature | Without Wrapper | With Wrapper |
|--------------------|--------------------|-----------------------------|
|--------------------|--------------------|-----------------------------|
| Basic usage | ✅ Works | ✅ Works |
| Collection factory | 1 line setup | Auto-configured |
| Code generation | bin/dto generate | artisan dto:generate |
Expand Down Expand Up @@ -193,28 +193,52 @@ class UsersController extends AppController

## Validation

The library doesn't include validation - use your framework's validator.
The library includes built-in DTO validation for:

- required fields
- PHP-native type enforcement
- common field rules such as `minLength`, `maxLength`, `min`, `max`, and `pattern`

Framework validators still matter for request validation, business rules, custom messages, and cross-field constraints. A practical setup is:

1. Let the DTO handle structural validation and type-safe hydration.
2. Use your framework validator for request-specific or domain-specific rules.
3. Optionally map `validationRules()` into framework-native rules if you want to reuse DTO metadata.

### Laravel

```php
use Illuminate\Support\Facades\Validator;

$dto = new UserDto($request->all(), ignoreMissing: true);
$dto = UserDto::createFromArray($request->all(), ignoreMissing: true);

$dtoRules = $dto->validationRules();

$validator = Validator::make($dto->toArray(), [
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'name' => [
'required',
'string',
'min:' . ($dtoRules['name']['minLength'] ?? 0),
'max:' . ($dtoRules['name']['maxLength'] ?? 255),
],
'email' => [
'required',
'email',
'unique:users',
],
]);

if ($validator->fails()) {
return response()->json(['errors' => $validator->errors()], 422);
}
```

`validationRules()` returns framework-agnostic metadata such as `minLength` and `pattern`. Laravel does not consume that structure directly, so map it into Laravel rule strings or objects first.

### Symfony

```php
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Validator\ValidatorInterface;

class UserController extends AbstractController
Expand All @@ -223,7 +247,8 @@ class UserController extends AbstractController
{
$dto = new UserDto(json_decode($request->getContent(), true));

// Validate the DTO array representation
// DTO construction already applies built-in field validation.
// Use Symfony Validator for richer request or business rules.
$violations = $validator->validate($dto->toArray(), new Assert\Collection([
'name' => [new Assert\NotBlank(), new Assert\Length(['max' => 255])],
'email' => [new Assert\NotBlank(), new Assert\Email()],
Expand All @@ -246,10 +271,10 @@ The CLI tool works the same way in any framework:

```bash
# Generate DTOs from config
bin/dto generate config/dto.php src/Dto App\\Dto
vendor/bin/dto generate --config-path=config/ --src-path=src/ --namespace=App

# Or with XML
bin/dto generate config/dto.xml src/Dto App\\Dto
vendor/bin/dto generate --config-path=config/ --src-path=src/ --namespace=App --format=xml
```

### Framework-Specific Paths
Expand All @@ -267,7 +292,7 @@ Add to your `composer.json` for convenience:
```json
{
"scripts": {
"dto:generate": "bin/dto generate config/dto.php src/Dto App\\Dto"
"dto:generate": "vendor/bin/dto generate --config-path=config/ --src-path=src/ --namespace=App"
}
}
```
Expand Down
7 changes: 5 additions & 2 deletions docs/guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Create `config/dto.xml`:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<dtos xmlns="https://github.com/php-collective/dto">
<dtos xmlns="php-collective-dto">
<dto name="User">
<field name="id" type="int" required="true"/>
<field name="name" type="string" required="true"/>
Expand Down Expand Up @@ -87,7 +87,7 @@ Choose your preferred format—all generate identical DTOs:

```xml [XML]
<?xml version="1.0" encoding="UTF-8"?>
<dtos xmlns="https://github.com/php-collective/dto">
<dtos xmlns="php-collective-dto">
<dto name="User">
<field name="name" type="string" required="true"/>
<field name="email" type="string"/>
Expand Down Expand Up @@ -271,5 +271,8 @@ parameters:

- [Examples](./examples) — Real-world usage patterns
- [Config Builder](./config-builder) — Fluent PHP API reference
- [Runtime API](./runtime-api) — Core DTO methods, serialization, and global options
- [Validation](./validation) — Field constraints and rules
- [Circular Dependencies](./circular-dependencies) — How to model recursive DTO graphs safely
- [TypeScript Generation](../reference/typescript) — Frontend type sync
- [JSON Schema Generation](../reference/jsonschema) — Export JSON Schema from DTO definitions
Loading
Loading