Auto Discovery Relations - Grazulex/laravel-arc GitHub Wiki
🔍 Auto-Discovery Relations
Auto-Discovery Relations is a powerful feature that automatically detects and maps Eloquent model relationships when generating DTOs. This eliminates manual work and ensures your DTOs accurately reflect your model structure.
🎯 Overview
The auto-discovery system analyzes your Eloquent models using reflection to identify relationships and automatically generate appropriate DTO properties with correct types and configurations.
Key Benefits
- Automatic Detection: No manual relationship configuration needed
- Smart Mapping: Intelligent type detection (nested vs collection)
- Safe Execution: Graceful error handling for broken relations
- Command Integration: Seamless Artisan command integration
- Accurate Types: Proper type hints and Property attributes
🛠️ How It Works
Relationship Detection Process
- Model Analysis: Scans model methods using reflection
- Relationship Identification: Detects Laravel relationship return types
- Type Mapping: Maps relationship types to DTO property types
- Property Generation: Creates appropriate Property attributes
Supported Relationships
Laravel Relationship | DTO Property Type | Description |
---|---|---|
HasOne |
nested |
Single related model → nested DTO |
BelongsTo |
nested |
Single parent model → nested DTO |
HasMany |
collection |
Multiple models → collection of DTOs |
BelongsToMany |
collection |
Many-to-many → collection of DTOs |
HasManyThrough |
collection |
Distant relations → collection of DTOs |
MorphOne |
nested |
Polymorphic single → nested DTO |
MorphMany |
collection |
Polymorphic multiple → collection of DTOs |
🚀 Usage
Basic Auto-Discovery
# Generate DTO with automatic relation detection
php artisan make:dto User --model=User --with-relations
Specific Relations Only
# Generate only specific relations
php artisan make:dto User --model=User --relations=posts,profile,orders
Exclude Relations
# Generate with relations but exclude some
php artisan make:dto User --model=User --with-relations --exclude-relations=secrets,tokens
📋 Example Models and Generated DTOs
Model Setup
// User Model
class User extends Model
{
public function profile(): HasOne
{
return $this->hasOne(Profile::class);
}
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
public function orders(): HasMany
{
return $this->hasMany(Order::class);
}
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class);
}
public function company(): BelongsTo
{
return $this->belongsTo(Company::class);
}
}
// Post Model
class Post extends Model
{
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class);
}
}
Auto-Generated User DTO
php artisan make:dto User --model=User --with-relations
Generated Result:
class UserDTO extends LaravelArcDTO
{
#[Property(type: 'int', required: true)]
public int $id;
#[Property(type: 'string', required: true, validation: 'max:255')]
public string $name;
#[Property(type: 'string', required: true, validation: 'email')]
public string $email;
#[Property(type: 'date', required: false)]
public ?Carbon $email_verified_at;
// Auto-discovered relations
#[Property(type: 'nested', class: ProfileDTO::class, required: false)]
public ?ProfileDTO $profile;
#[Property(type: 'collection', class: PostDTO::class, required: false, default: [])]
public array $posts;
#[Property(type: 'collection', class: OrderDTO::class, required: false, default: [])]
public array $orders;
#[Property(type: 'collection', class: RoleDTO::class, required: false, default: [])]
public array $roles;
#[Property(type: 'nested', class: CompanyDTO::class, required: false)]
public ?CompanyDTO $company;
#[Property(type: 'date', required: false)]
public ?Carbon $created_at;
#[Property(type: 'date', required: false)]
public ?Carbon $updated_at;
}
Selective Relation Generation
php artisan make:dto User --model=User --relations=profile,posts
Generated Result:
class UserDTO extends LaravelArcDTO
{
#[Property(type: 'int', required: true)]
public int $id;
#[Property(type: 'string', required: true, validation: 'max:255')]
public string $name;
#[Property(type: 'string', required: true, validation: 'email')]
public string $email;
// Only specified relations
#[Property(type: 'nested', class: ProfileDTO::class, required: false)]
public ?ProfileDTO $profile;
#[Property(type: 'collection', class: PostDTO::class, required: false, default: [])]
public array $posts;
// Other relations excluded
#[Property(type: 'date', required: false)]
public ?Carbon $created_at;
}
🔧 Advanced Configuration
Custom Relationship Mapping
You can customize how relationships are mapped:
// In your service provider
public function boot()
{
// Register custom relationship mappers
DTOGenerator::mapRelationship('hasOneThrough', 'nested');
DTOGenerator::mapRelationship('hasManyThrough', 'collection');
}
Relationship Filtering
// Custom filtering logic
DTOGenerator::filterRelations(function ($relationName, $relationType) {
// Skip certain relations
if (in_array($relationName, ['secrets', 'internal_data'])) {
return false;
}
// Only include specific types
return in_array($relationType, ['HasOne', 'BelongsTo', 'HasMany']);
});
🛡️ Smart Analysis Features
Method Detection
The auto-discovery system intelligently identifies relationship methods:
class User extends Model
{
// ✅ Detected - Returns relationship
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
// ✅ Detected - Returns relationship
public function profile()
{
return $this->hasOne(Profile::class);
}
// ❌ Skipped - Getter method
public function getFullNameAttribute(): string
{
return $this->first_name . ' ' . $this->last_name;
}
// ❌ Skipped - Business logic method
public function calculateAge(): int
{
return now()->diffInYears($this->birth_date);
}
// ❌ Skipped - Scope method
public function scopeActive($query)
{
return $query->where('is_active', true);
}
}
Error Handling
Graceful handling of problematic relationships:
class User extends Model
{
// This relationship has issues but won't break generation
public function brokenRelation()
{
// Missing return statement or invalid relationship
$this->hasMany(NonExistentModel::class);
}
// This will be skipped gracefully
public function dynamicRelation()
{
if (some_condition()) {
return $this->hasMany(PostA::class);
}
return $this->hasMany(PostB::class);
}
}
🎯 Real-World Examples
E-commerce System
// Models
class Order extends Model
{
public function customer(): BelongsTo
{
return $this->belongsTo(User::class, 'customer_id');
}
public function items(): HasMany
{
return $this->hasMany(OrderItem::class);
}
public function payments(): HasMany
{
return $this->hasMany(Payment::class);
}
public function shippingAddress(): HasOne
{
return $this->hasOne(Address::class, 'order_id')->where('type', 'shipping');
}
}
// Generated DTO
php artisan make:dto Order --model=Order --with-relations
Result:
class OrderDTO extends LaravelArcDTO
{
#[Property(type: 'string', required: true)]
public string $order_number;
#[Property(type: 'nested', class: UserDTO::class, required: true)]
public UserDTO $customer;
#[Property(type: 'collection', class: OrderItemDTO::class, required: false, default: [])]
public array $items;
#[Property(type: 'collection', class: PaymentDTO::class, required: false, default: [])]
public array $payments;
#[Property(type: 'nested', class: AddressDTO::class, required: false)]
public ?AddressDTO $shippingAddress;
#[Property(type: 'enum', class: OrderStatus::class, required: false)]
public ?OrderStatus $status;
}
Blog System
// Generate complete blog post DTO with all relations
php artisan make:dto BlogPost --model=Post --with-relations
// Result includes:
// - author (User) → nested UserDTO
// - comments → collection of CommentDTO
// - tags → collection of TagDTO
// - categories → collection of CategoryDTO
⚡ Performance Considerations
Lazy Loading Prevention
class UserDTO extends LaravelArcDTO
{
// Relations are marked as optional to prevent eager loading
#[Property(type: 'collection', class: PostDTO::class, required: false, default: [])]
public array $posts;
// Use factory methods for controlled loading
public static function fromModelWithPosts(User $user): self
{
$user->load('posts');
return new self($user->toArray());
}
public static function fromModelBasic(User $user): self
{
return new self($user->only(['id', 'name', 'email']));
}
}
Selective Loading
class UserService
{
public function getUserWithRelations(int $id, array $relations = []): UserDTO
{
$user = User::with($relations)->findOrFail($id);
return new UserDTO($user->toArray());
}
public function getUserBasic(int $id): UserDTO
{
$user = User::findOrFail($id);
return new UserDTO($user->only(['id', 'name', 'email']));
}
}
🛠️ Testing Auto-Discovery
Unit Tests
class AutoDiscoveryTest extends TestCase
{
public function test_detects_has_one_relationship(): void
{
$generator = new DTOGenerator();
$relations = $generator->discoverRelations(User::class);
$this->assertArrayHasKey('profile', $relations);
$this->assertEquals('nested', $relations['profile']['type']);
$this->assertEquals(ProfileDTO::class, $relations['profile']['class']);
}
public function test_detects_has_many_relationship(): void
{
$generator = new DTOGenerator();
$relations = $generator->discoverRelations(User::class);
$this->assertArrayHasKey('posts', $relations);
$this->assertEquals('collection', $relations['posts']['type']);
$this->assertEquals(PostDTO::class, $relations['posts']['class']);
}
public function test_skips_non_relationship_methods(): void
{
$generator = new DTOGenerator();
$relations = $generator->discoverRelations(User::class);
$this->assertArrayNotHasKey('getFullNameAttribute', $relations);
$this->assertArrayNotHasKey('scopeActive', $relations);
}
}
Integration Tests
class DTOGenerationTest extends TestCase
{
public function test_generates_dto_with_relations(): void
{
Artisan::call('make:dto', [
'name' => 'TestUser',
'--model' => 'User',
'--with-relations' => true
]);
$output = Artisan::output();
$this->assertStringContainsString('DTO created successfully', $output);
$this->assertTrue(class_exists('App\DTOs\TestUserDTO'));
}
}
📚 Best Practices
1. Use Selective Relations
// ✅ Good - Only include needed relations
php artisan make:dto User --model=User --relations=profile,posts
// ❌ Avoid - Including all relations can be overwhelming
php artisan make:dto User --model=User --with-relations
2. Consider Performance
// ✅ Good - Separate DTOs for different use cases
class UserSummaryDTO extends LaravelArcDTO
{
// Basic user info only
public string $name;
public string $email;
}
class UserDetailDTO extends LaravelArcDTO
{
// Full user with relations
public string $name;
public string $email;
public ?ProfileDTO $profile;
public array $posts;
}
3. Document Relations
class UserDTO extends LaravelArcDTO
{
/**
* User's profile information
* Loaded from: User::profile() relationship
*/
#[Property(type: 'nested', class: ProfileDTO::class, required: false)]
public ?ProfileDTO $profile;
/**
* All posts authored by this user
* Note: Can be memory intensive for prolific authors
*/
#[Property(type: 'collection', class: PostDTO::class, required: false, default: [])]
public array $posts;
}
🔗 Related Pages
- Advanced Features - Overview of all advanced features
- Property Attributes - Learn about Property configurations
- Examples Gallery - Real-world usage examples
- Debug & Analysis Tools - Analyze generated DTOs
- Best Practices - Recommended patterns
⬅️ Back to: Advanced Features | ➡️ Next: Smart Validation Rules