Skip to content

Collections

Jean-Marc Strauven edited this page Aug 6, 2025 · 2 revisions

πŸ“Š Collections

Laravel Arc provides powerful collection support for working with arrays of DTOs. Learn how to handle lists, manage bulk operations, and leverage Laravel's Collection methods.

Collection Basics

Collections in Laravel Arc are arrays of DTOs that behave like Laravel Collections but with additional DTO-specific functionality.

// Create collection of DTOs
$users = collect([
    UserDto::from(['name' => 'John', 'email' => '[email protected]']),
    UserDto::from(['name' => 'Jane', 'email' => '[email protected]']),
    UserDto::from(['name' => 'Bob', 'email' => '[email protected]'])
]);

// Collection methods work seamlessly
$activeUsers = $users->filter(fn($user) => $user->status === 'active');
$emails = $users->pluck('email');
$totalAge = $users->sum('age');

Creating Collections

From Arrays

// From array of arrays
$usersData = [
    ['name' => 'John', 'email' => '[email protected]', 'age' => 30],
    ['name' => 'Jane', 'email' => '[email protected]', 'age' => 25],
    ['name' => 'Bob', 'email' => '[email protected]', 'age' => 35]
];

$users = UserDto::collection($usersData);
// or
$users = collect($usersData)->map(fn($data) => UserDto::from($data));

From Database Results

// From Eloquent collection
$users = User::all()->map(fn($user) => UserDto::from($user->toArray()));

// From query results
$users = DB::table('users')
    ->get()
    ->map(fn($user) => UserDto::from((array) $user));

From API Responses

// From JSON API response
$response = Http::get('https://api.example.com/users');
$users = collect($response->json('data'))
    ->map(fn($userData) => UserDto::from($userData));

YAML Collection Definitions

Define collections directly in YAML:

header:
  class: UserCollectionDto
  namespace: App\DTOs\Collections

fields:
  users:
    type: array
    items:
      type: object
      class: UserDto
    min_items: 1
    max_items: 100
    
  metadata:
    type: object
    properties:
      total: { type: integer }
      page: { type: integer }
      per_page: { type: integer }

Generated collection DTO:

final readonly class UserCollectionDto
{
    public function __construct(
        public Collection $users,  // Collection<UserDto>
        public array $metadata,
    ) {}
    
    public function getUsersCount(): int
    {
        return $this->users->count();
    }
    
    public function getActiveUsers(): Collection
    {
        return $this->users->filter(fn($user) => $user->status === 'active');
    }
}

Collection Operations

Filtering and Searching

$users = UserDto::collection($usersData);

// Filter by property
$adults = $users->filter(fn($user) => $user->age >= 18);

// Filter by multiple conditions
$activeAdults = $users->filter(function($user) {
    return $user->age >= 18 && $user->status === 'active';
});

// Search by email domain
$companyUsers = $users->filter(fn($user) => 
    str_ends_with($user->email, '@company.com')
);

// Find specific user
$john = $users->firstWhere('name', 'John');
$adminUsers = $users->where('role', 'admin');

Transformations

// Map to different format
$userSummaries = $users->map(fn($user) => [
    'id' => $user->id,
    'display_name' => $user->getDisplayName(),
    'is_admin' => $user->hasRole('admin')
]);

// Transform to another DTO
$profiles = $users->map(fn($user) => UserProfileDto::from([
    'user_id' => $user->id,
    'name' => $user->name,
    'email' => $user->email
]));

// Flatten nested collections
$allAddresses = $users
    ->pluck('addresses')
    ->flatten()
    ->map(fn($addr) => AddressDto::from($addr));

Aggregations

// Basic aggregations
$totalUsers = $users->count();
$averageAge = $users->avg('age');
$oldestUser = $users->max('age');
$youngestUser = $users->min('age');

// Group by property
$usersByRole = $users->groupBy('role');
$usersByDepartment = $users->groupBy(fn($user) => $user->department);

// Count by group
$roleCounts = $users->countBy('role');
// Result: ['admin' => 2, 'user' => 15, 'moderator' => 3]

// Custom aggregations
$totalSalary = $users->sum(fn($user) => $user->salary);
$departmentStats = $users->groupBy('department')->map(function($deptUsers) {
    return [
        'count' => $deptUsers->count(),
        'avg_age' => $deptUsers->avg('age'),
        'total_salary' => $deptUsers->sum('salary')
    ];
});

Advanced Collection Features

Pagination Support

# Define paginated collection
header:
  class: PaginatedUserCollectionDto
  namespace: App\DTOs\Collections

fields:
  data:
    type: array
    items: { type: object, class: UserDto }
    
  pagination:
    type: object
    properties:
      current_page: { type: integer }
      per_page: { type: integer }
      total: { type: integer }
      last_page: { type: integer }
      has_more_pages: { type: boolean }

Usage:

$paginatedUsers = PaginatedUserCollectionDto::from([
    'data' => $usersData,
    'pagination' => [
        'current_page' => 1,
        'per_page' => 10,
        'total' => 50,
        'last_page' => 5,
        'has_more_pages' => true
    ]
]);

// Access paginated data
$users = $paginatedUsers->data;  // Collection<UserDto>
$hasMore = $paginatedUsers->pagination['has_more_pages'];

Nested Collections

# Order with items collection
header:
  class: OrderDto
  namespace: App\DTOs\Orders

fields:
  order_number: { type: string, required: true }
  customer_email: { type: email, required: true }
  
  items:
    type: array
    items: { type: object, class: OrderItemDto }
    min_items: 1
    
  shipping_addresses:
    type: array
    items: { type: object, class: AddressDto }
    max_items: 3

Usage:

$order = OrderDto::from([
    'order_number' => 'ORD-123456',
    'customer_email' => '[email protected]',
    'items' => [
        ['product_id' => 1, 'quantity' => 2, 'price' => 29.99],
        ['product_id' => 2, 'quantity' => 1, 'price' => 15.50]
    ],
    'shipping_addresses' => [
        ['street' => '123 Main St', 'city' => 'Boston', 'zip' => '02101']
    ]
]);

// Work with nested collections
$totalValue = $order->items->sum(fn($item) => $item->quantity * $item->price);
$productIds = $order->items->pluck('product_id');
$primaryAddress = $order->shipping_addresses->first();

Collection Validation

# Validated collection with constraints
fields:
  team_members:
    type: array
    items: { type: object, class: TeamMemberDto }
    min_items: 2    # Team must have at least 2 members
    max_items: 10   # Team cannot exceed 10 members
    validation:
      - "unique_emails"  # Custom validation rule
      - "at_least_one_admin"

Custom collection validation:

// In a service provider
Validator::extend('unique_emails', function ($attribute, $value, $parameters, $validator) {
    $emails = collect($value)->pluck('email');
    return $emails->count() === $emails->unique()->count();
});

Validator::extend('at_least_one_admin', function ($attribute, $value, $parameters, $validator) {
    return collect($value)->contains('role', 'admin');
});

Collection Export and Import

Bulk Export

$users = UserDto::collection($usersData);

// Export entire collection
$json = $users->toJson();
$csv = $users->toCsv();
$xml = $users->toXml();

// Export with custom format
$excel = $users->toExcel([
    'filename' => 'users_export.xlsx',
    'sheets' => [
        'Users' => $users->toArray(),
        'Summary' => [
            'total_users' => $users->count(),
            'active_users' => $users->where('status', 'active')->count(),
            'average_age' => $users->avg('age')
        ]
    ]
]);

Selective Export

// Export specific fields only
$userSummary = $users->map->only(['name', 'email', 'status'])->toArray();

// Export with computed fields
$usersWithAge = $users->map(function($user) {
    return $user->toArray() + [
        'age_group' => $user->getAgeGroup(),
        'is_senior' => $user->age >= 65
    ];
})->toJson();

Bulk Import

// Import from CSV
$csvData = File::get('users.csv');
$users = collect(str_getcsv($csvData, "\n"))
    ->map(fn($row) => str_getcsv($row))
    ->map(fn($row) => UserDto::from([
        'name' => $row[0],
        'email' => $row[1],
        'age' => (int) $row[2]
    ]));

// Import with validation
$validUsers = collect();
$errors = collect();

foreach ($usersData as $userData) {
    try {
        $user = UserDto::fromValidated($userData);
        $validUsers->push($user);
    } catch (ValidationException $e) {
        $errors->push(['data' => $userData, 'errors' => $e->errors()]);
    }
}

Performance Considerations

Lazy Collections

For large datasets, use lazy collections:

// Process large dataset without memory issues
$users = LazyCollection::make(function() {
    $page = 1;
    do {
        $results = DB::table('users')
            ->skip(($page - 1) * 1000)
            ->take(1000)
            ->get();
            
        foreach ($results as $user) {
            yield UserDto::from((array) $user);
        }
        
        $page++;
    } while ($results->count() === 1000);
});

// Process one at a time
$users->filter(fn($user) => $user->isActive())
      ->each(fn($user) => $user->sendWelcomeEmail());

Chunk Processing

// Process collections in chunks
$users = UserDto::collection($largeUsersData);

$users->chunk(100)->each(function($chunk) {
    // Process 100 users at a time
    $this->emailService->sendBulkNotifications($chunk);
});

Memory Optimization

// Clear processed items from memory
$processedCount = 0;
$users->each(function($user) use (&$processedCount) {
    $this->processUser($user);
    $processedCount++;
    
    // Clear every 1000 items
    if ($processedCount % 1000 === 0) {
        gc_collect_cycles();
    }
});

Real-World Examples

API Response Collection

class UserApiController extends Controller
{
    public function index(Request $request)
    {
        $users = User::query()
            ->when($request->search, fn($q) => $q->where('name', 'like', "%{$request->search}%"))
            ->when($request->role, fn($q) => $q->where('role', $request->role))
            ->paginate($request->per_page ?? 15);
            
        $userDtos = $users->getCollection()
            ->map(fn($user) => UserResponseDto::from($user->toArray()));
            
        return PaginatedUserCollectionDto::from([
            'data' => $userDtos,
            'pagination' => [
                'current_page' => $users->currentPage(),
                'per_page' => $users->perPage(),
                'total' => $users->total(),
                'last_page' => $users->lastPage(),
                'has_more_pages' => $users->hasMorePages()
            ]
        ])->toArray();
    }
}

Bulk Data Processing

class UserImportService
{
    public function importFromCsv(string $csvPath): array
    {
        $csvData = $this->parseCsv($csvPath);
        
        $results = [
            'successful' => collect(),
            'failed' => collect(),
            'duplicates' => collect()
        ];
        
        collect($csvData)->each(function($row) use (&$results) {
            try {
                // Check for duplicates
                if (User::where('email', $row['email'])->exists()) {
                    $results['duplicates']->push($row);
                    return;
                }
                
                // Validate and create DTO
                $userDto = UserDto::fromValidated($row);
                
                // Create user
                User::create($userDto->toArray());
                $results['successful']->push($userDto);
                
            } catch (ValidationException $e) {
                $results['failed']->push([
                    'data' => $row,
                    'errors' => $e->errors()
                ]);
            }
        });
        
        return [
            'imported' => $results['successful']->count(),
            'failed' => $results['failed']->count(),
            'duplicates' => $results['duplicates']->count(),
            'errors' => $results['failed']->toArray()
        ];
    }
}

Report Generation

class UserReportService
{
    public function generateMonthlyReport(): UserReportDto
    {
        $users = User::whereMonth('created_at', now()->month)
            ->get()
            ->map(fn($user) => UserDto::from($user->toArray()));
            
        return UserReportDto::from([
            'period' => now()->format('F Y'),
            'total_users' => $users->count(),
            'new_users' => $users->where('created_at', '>=', now()->startOfMonth())->count(),
            'active_users' => $users->where('status', 'active')->count(),
            'users_by_role' => $users->countBy('role'),
            'average_age' => $users->avg('age'),
            'top_domains' => $users
                ->pluck('email')
                ->map(fn($email) => Str::after($email, '@'))
                ->countBy()
                ->sortDesc()
                ->take(10)
                ->toArray()
        ]);
    }
}

What's Next?

Now that you understand collections:


Collections make working with multiple DTOs as elegant as working with single objects. Laravel Arc's collection support brings the power of Laravel Collections to your type-safe DTOs. πŸ“Š

πŸš€ Laravel Arc Wiki

🏠 Home

πŸš€ Getting Started

πŸ“š Core Concepts

πŸ—οΈ Advanced Features

βš™οΈ Configuration & CLI

🌐 Real-World Examples


🎯 Key Concepts

YAML β†’ DTO β†’ Type-Safe Code

Laravel Arc transforms your YAML definitions into powerful PHP DTOs with automatic validation, field transformers, and behavioral traits.

πŸ”— Quick Links

Clone this wiki locally