Best Practices - Grazulex/laravel-arc GitHub Wiki
π Best Practices
This guide covers recommended patterns, conventions, and best practices for using Laravel Arc effectively in your applications. Following these practices will help you build maintainable, performant, and scalable DTO implementations.
π― General Principles
1. Single Responsibility
Each DTO should represent a single, cohesive data structure.
// β
Good - Focused on user data
class UserDTO extends LaravelArcDTO
{
public string $name;
public string $email;
public int $age;
}
// β Avoid - Mixed concerns
class UserOrderProfileDTO extends LaravelArcDTO
{
public string $user_name;
public string $order_number;
public string $profile_bio;
public float $order_total;
}
2. Immutability When Possible
Prefer immutable DTOs for data integrity.
// β
Good - Immutable data transfer
class ApiResponseDTO extends LaravelArcDTO
{
#[Property(type: 'string', required: true)]
public readonly string $status;
#[Property(type: 'array', required: true)]
public readonly array $data;
#[Property(type: 'string', required: false)]
public readonly ?string $message;
}
// β Avoid - Mutable critical data
class PaymentDTO extends LaravelArcDTO
{
public float $amount; // Could be accidentally modified
public string $currency;
}
3. Explicit Over Implicit
Be explicit about types, requirements, and validation.
// β
Good - Clear and explicit
class ProductDTO extends LaravelArcDTO
{
#[Property(type: 'string', required: true, validation: 'max:255')]
public string $name;
#[Property(type: 'float', required: true, validation: 'min:0.01|max:999999.99')]
public float $price;
#[Property(type: 'bool', required: false, default: true)]
public bool $is_active;
}
// β Avoid - Unclear intentions
class ProductDTO extends LaravelArcDTO
{
public $name; // No type hint
public $price; // Could be string or float
public $is_active; // Default unclear
}
π§ Property Definitions
Type Safety
// β
Excellent - Full type safety
class UserDTO extends LaravelArcDTO
{
#[Property(type: 'string', required: true, validation: 'max:255')]
public string $name;
#[Property(type: 'int', required: true, validation: 'min:18|max:120')]
public int $age;
#[Property(type: 'enum', class: UserStatus::class, default: UserStatus::ACTIVE)]
public UserStatus $status;
}
// β Poor - Weak typing
class UserDTO extends LaravelArcDTO
{
public $name; // No type or validation
public $age; // Could be string '25'
public $status; // Unclear what values are valid
}
Nullable Properties
// β
Good - Clear nullable intent
class ProfileDTO extends LaravelArcDTO
{
#[Property(type: 'string', required: true)]
public string $username;
#[Property(type: 'string', required: false)]
public ?string $bio; // Clearly optional
#[Property(type: 'string', required: false)]
public ?string $avatar_url; // Clearly optional
}
// β Avoid - Unclear null handling
class ProfileDTO extends LaravelArcDTO
{
public string $username;
public string $bio; // Required but might be empty?
public string $avatar_url; // Required but might not exist?
}
Default Values
// β
Good - Meaningful defaults
class SettingsDTO extends LaravelArcDTO
{
#[Property(type: 'bool', required: false, default: true)]
public bool $email_notifications;
#[Property(type: 'string', required: false, default: 'light')]
public string $theme;
#[Property(type: 'array', required: false, default: [])]
public array $preferences;
}
// β Avoid - No clear defaults
class SettingsDTO extends LaravelArcDTO
{
public ?bool $email_notifications; // null vs false?
public ?string $theme; // What's the default?
public ?array $preferences; // null vs empty array?
}
β Validation Best Practices
Comprehensive Validation
// β
Excellent - Comprehensive validation
class RegistrationDTO extends LaravelArcDTO
{
#[Property(
type: 'string',
required: true,
validation: 'required|string|min:2|max:50|regex:/^[\pL\s\-]+$/u',
transform: [TrimTransformer::class]
)]
public string $first_name;
#[Property(
type: 'string',
required: true,
validation: 'required|email:rfc,dns|max:254|unique:users,email',
transform: [TrimTransformer::class, LowercaseTransformer::class]
)]
public string $email;
#[Property(
type: 'string',
required: true,
validation: 'required|string|min:8|regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/',
transform: [HashTransformer::class]
)]
public string $password;
}
// β Poor - Minimal validation
class RegistrationDTO extends LaravelArcDTO
{
#[Property(type: 'string', required: true)]
public string $first_name; // No length or format rules
#[Property(type: 'string', required: true, validation: 'email')]
public string $email; // Basic email only
#[Property(type: 'string', required: true)]
public string $password; // No security requirements
}
Security-First Validation
// β
Security-conscious
class AdminUserDTO extends LaravelArcDTO
{
#[Property(
type: 'string',
required: true,
validation: 'required|string|min:12|regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/',
transform: [HashTransformer::class]
)]
public string $password;
#[Property(
type: 'string',
required: true,
validation: 'required|string|in:admin,super_admin',
)]
public string $role;
#[Property(
type: 'array',
required: false,
validation: 'array|max:10',
default: []
)]
public array $permissions;
}
π Transformation Patterns
Single Responsibility Transformers
// β
Good - Each transformer has one job
class UserDTO extends LaravelArcDTO
{
#[Property(
type: 'string',
required: true,
transform: [
TrimTransformer::class, // 1. Remove whitespace
LowercaseTransformer::class, // 2. Normalize case
EmailNormalizeTransformer::class // 3. Email-specific normalization
]
)]
public string $email;
}
// β Avoid - Complex multi-purpose transformer
class UserDTO extends LaravelArcDTO
{
#[Property(
type: 'string',
required: true,
transform: [ComplexEmailTransformer::class] // Does everything at once
)]
public string $email;
}
Logical Order
// β
Good - Logical transformation order
class ProductDTO extends LaravelArcDTO
{
#[Property(
type: 'string',
transform: [
TrimTransformer::class, // 1. Clean input
LowercaseTransformer::class, // 2. Normalize case
SlugTransformer::class // 3. Generate slug
]
)]
public string $slug;
}
// β Poor - Illogical order
class ProductDTO extends LaravelArcDTO
{
#[Property(
type: 'string',
transform: [
SlugTransformer::class, // Creates slug from dirty input
TrimTransformer::class, // Too late to clean
LowercaseTransformer::class // Might break slug
]
)]
public string $slug;
}
π Relationship Patterns
Clear Relationship Types
// β
Good - Clear relationship intent
class OrderDTO extends LaravelArcDTO
{
#[Property(type: 'nested', class: CustomerDTO::class, required: true)]
public CustomerDTO $customer; // Single customer
#[Property(type: 'collection', class: OrderItemDTO::class, required: false, default: [])]
public array $items; // Multiple items
#[Property(type: 'nested', class: AddressDTO::class, required: false)]
public ?AddressDTO $shipping_address; // Optional address
}
// β Unclear - Ambiguous relationships
class OrderDTO extends LaravelArcDTO
{
public $customer; // Array or object?
public $items; // What type of items?
public $address; // Which address?
}
Avoid Circular Dependencies
// β
Good - No circular references
class UserDTO extends LaravelArcDTO
{
public string $name;
public string $email;
#[Property(type: 'collection', class: PostDTO::class, default: [])]
public array $posts;
}
class PostDTO extends LaravelArcDTO
{
public string $title;
public string $content;
public int $user_id; // Reference by ID, not object
}
// β Avoid - Circular dependency
class UserDTO extends LaravelArcDTO
{
#[Property(type: 'collection', class: PostDTO::class)]
public array $posts;
}
class PostDTO extends LaravelArcDTO
{
#[Property(type: 'nested', class: UserDTO::class)] // Circular!
public UserDTO $user;
}
π Naming Conventions
Consistent DTO Naming
// β
Good - Clear, consistent naming
class CreateUserDTO extends LaravelArcDTO { } // For creation
class UpdateUserDTO extends LaravelArcDTO { } // For updates
class UserResponseDTO extends LaravelArcDTO { } // For API responses
class UserSummaryDTO extends LaravelArcDTO { } // For listings
// β Avoid - Inconsistent naming
class UserCreateData extends LaravelArcDTO { } // Mixed convention
class UserUpdate extends LaravelArcDTO { } // Missing DTO suffix
class UserAPIResponse extends LaravelArcDTO { } // Inconsistent acronym
class UserList extends LaravelArcDTO { } // Unclear purpose
Property Naming
// β
Good - Clear, descriptive names
class OrderDTO extends LaravelArcDTO
{
public string $order_number;
public Carbon $created_at;
public float $total_amount;
public string $shipping_method;
public bool $is_paid;
}
// β Avoid - Unclear or abbreviated names
class OrderDTO extends LaravelArcDTO
{
public string $num; // What number?
public Carbon $ts; // Timestamp of what?
public float $amt; // Amount of what?
public string $ship; // Shipping what?
public bool $paid; // Fully paid or partially?
}
ποΈ Architecture Patterns
Layered DTOs
// β
Good - Layered approach
// Domain layer - Core business data
class UserDomainDTO extends LaravelArcDTO
{
public string $name;
public string $email;
public UserStatus $status;
}
// API layer - External interface
class UserApiDTO extends LaravelArcDTO
{
public string $name;
public string $email;
public string $status; // String representation
public string $created_at; // Formatted date
}
// Database layer - Persistence
class UserDatabaseDTO extends LaravelArcDTO
{
public string $name;
public string $email;
public string $status;
public ?Carbon $created_at;
public ?Carbon $updated_at;
}
Factory Pattern
// β
Good - Factory for complex creation
class UserDTOFactory
{
public static function fromModel(User $user): UserDTO
{
return new UserDTO([
'name' => $user->name,
'email' => $user->email,
'status' => $user->status,
'created_at' => $user->created_at
]);
}
public static function fromApiRequest(array $data): CreateUserDTO
{
return new CreateUserDTO([
'name' => $data['name'],
'email' => $data['email'],
'password' => $data['password']
]);
}
public static function forTesting(array $overrides = []): UserDTO
{
return new UserDTO(array_merge([
'name' => fake()->name(),
'email' => fake()->email(),
'status' => UserStatus::ACTIVE
], $overrides));
}
}
β‘ Performance Best Practices
Lazy Loading
// β
Good - Lazy loading for expensive operations
class UserDTO extends LaravelArcDTO
{
public string $name;
public string $email;
private ?array $_posts = null;
public function getPosts(): array
{
if ($this->_posts === null) {
$this->_posts = Post::where('user_id', $this->id)
->get()
->map(fn($post) => new PostDTO($post->toArray()))
->toArray();
}
return $this->_posts;
}
}
// β Avoid - Eager loading everything
class UserDTO extends LaravelArcDTO
{
public string $name;
public string $email;
#[Property(type: 'collection', class: PostDTO::class)]
public array $posts; // Always loaded, even if not needed
#[Property(type: 'collection', class: CommentDTO::class)]
public array $comments; // More unnecessary data
}
Memory Efficiency
// β
Good - Memory efficient
class UserSummaryDTO extends LaravelArcDTO
{
public int $id;
public string $name;
public string $email;
// Only essential data for listings
}
class UserDetailDTO extends LaravelArcDTO
{
public int $id;
public string $name;
public string $email;
public ?ProfileDTO $profile;
public array $preferences;
// Full data for detail views
}
// β Avoid - One size fits all
class UserDTO extends LaravelArcDTO
{
// Always includes everything, even for simple listings
public int $id;
public string $name;
public string $email;
public ?ProfileDTO $profile;
public array $preferences;
public array $posts;
public array $comments;
public array $followers;
public array $following;
}
π§ͺ Testing Best Practices
Comprehensive Testing
class UserDTOTest extends TestCase
{
public function test_creates_valid_user_dto(): void
{
$data = [
'name' => 'John Doe',
'email' => '[email protected]',
'age' => 30
];
$user = new UserDTO($data);
$this->assertEquals('John Doe', $user->name);
$this->assertEquals('[email protected]', $user->email);
$this->assertEquals(30, $user->age);
}
public function test_validates_required_fields(): void
{
$this->expectException(ValidationException::class);
new UserDTO([]); // Should fail - missing required fields
}
public function test_applies_transformations(): void
{
$user = new UserDTO([
'name' => ' John Doe ',
'email' => ' [email protected] ',
'age' => 30
]);
$this->assertEquals('John Doe', $user->name); // Trimmed
$this->assertEquals('[email protected]', $user->email); // Trimmed + lowercased
}
public function test_serialization(): void
{
$user = UserDTOFactory::forTesting();
$array = $user->toArray();
$json = $user->toJson();
$recreated = new UserDTO(json_decode($json, true));
$this->assertEquals($user->toArray(), $recreated->toArray());
}
}
Edge Case Testing
class UserDTOEdgeCaseTest extends TestCase
{
public function test_handles_null_values_correctly(): void
{
$user = new UserDTO([
'name' => 'John',
'email' => '[email protected]',
'bio' => null // Optional field
]);
$this->assertNull($user->bio);
}
public function test_handles_unicode_content(): void
{
$user = new UserDTO([
'name' => 'δΈζεε', // Chinese characters
'email' => 'test@δΎε.com',
'age' => 25
]);
$this->assertEquals('δΈζεε', $user->name);
}
public function test_boundary_values(): void
{
// Test age boundaries
$user = new UserDTO([
'name' => 'Test',
'email' => '[email protected]',
'age' => 0 // Minimum allowed age
]);
$this->assertEquals(0, $user->age);
$this->expectException(ValidationException::class);
new UserDTO([
'name' => 'Test',
'email' => '[email protected]',
'age' => 151 // Above maximum
]);
}
}
π Organization & Structure
Directory Structure
app/
βββ DTOs/
β βββ User/
β β βββ CreateUserDTO.php
β β βββ UpdateUserDTO.php
β β βββ UserResponseDTO.php
β β βββ UserSummaryDTO.php
β βββ Order/
β β βββ CreateOrderDTO.php
β β βββ OrderDTO.php
β β βββ OrderItemDTO.php
β βββ Common/
β βββ AddressDTO.php
β βββ PaginationDTO.php
βββ Factories/
β βββ UserDTOFactory.php
β βββ OrderDTOFactory.php
βββ Transformers/
βββ SlugTransformer.php
βββ PhoneTransformer.php
Namespace Organization
// β
Good - Clear namespace structure
namespace App\DTOs\User;
namespace App\DTOs\Order;
namespace App\DTOs\Common;
namespace App\Factories\DTO;
namespace App\Transformers\DTO;
// β Avoid - Flat namespace
namespace App\DTOs; // Everything in one namespace
π Documentation
Self-Documenting Code
// β
Good - Clear and documented
class UserRegistrationDTO extends LaravelArcDTO
{
/**
* User's full name as provided during registration
* Must be 2-50 characters, letters and spaces only
*/
#[Property(
type: 'string',
required: true,
validation: 'required|string|min:2|max:50|regex:/^[\pL\s]+$/u',
transform: [TrimTransformer::class]
)]
public string $full_name;
/**
* Email address for account creation
* Must be unique and pass DNS validation
*/
#[Property(
type: 'string',
required: true,
validation: 'required|email:rfc,dns|max:254|unique:users,email',
transform: [TrimTransformer::class, LowercaseTransformer::class]
)]
public string $email;
}
π Related Pages
- Property Attributes - Detailed attribute documentation
- Transformation Pipeline - Transformation patterns
- Smart Validation Rules - Validation best practices
- Debug & Analysis Tools - Testing and optimization
- Examples Gallery - Real-world implementations
β¬ οΈ Back to: Debug & Analysis Tools | β‘οΈ Next: FAQ