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

  1. Model Analysis: Scans model methods using reflection
  2. Relationship Identification: Detects Laravel relationship return types
  3. Type Mapping: Maps relationship types to DTO property types
  4. 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


⬅️ Back to: Advanced Features | ➡️ Next: Smart Validation Rules