Navigation

Php

How to Use PHP's Constructor Property Promotion

Use PHP 8.0+ constructor property promotion to reduce boilerplate code. Declare and initialize class properties directly in constructor parameters.

Table Of Contents

Problem

You need to create classes with many properties that are initialized from constructor parameters, resulting in repetitive property declarations and assignments.

Solution

// Traditional way (verbose)
class UserOld {
    private string $name;
    private string $email;
    private int $age;
    private bool $active;
    
    public function __construct(string $name, string $email, int $age, bool $active = true) {
        $this->name = $name;
        $this->email = $email;
        $this->age = $age;
        $this->active = $active;
    }
    
    public function getName(): string {
        return $this->name;
    }
    
    public function getEmail(): string {
        return $this->email;
    }
}

// Constructor property promotion (clean)
class User {
    public function __construct(
        private string $name,
        private string $email,
        private int $age,
        private bool $active = true
    ) {}
    
    public function getName(): string {
        return $this->name;
    }
    
    public function getEmail(): string {
        return $this->email;
    }
    
    public function getAge(): int {
        return $this->age;
    }
    
    public function isActive(): bool {
        return $this->active;
    }
}

$user = new User('John Doe', 'john@example.com', 30);

// Mixed promoted and regular properties
class Product {
    private float $calculatedPrice;
    
    public function __construct(
        public readonly string $id,
        public string $name,
        private float $basePrice,
        private float $taxRate = 0.1,
        array $categories = []
    ) {
        $this->calculatedPrice = $this->basePrice * (1 + $this->taxRate);
        $this->categories = array_map('strtolower', $categories);
    }
    
    private array $categories;
    
    public function getPrice(): float {
        return $this->calculatedPrice;
    }
    
    public function getCategories(): array {
        return $this->categories;
    }
}

$product = new Product(
    id: 'PROD-001',
    name: 'Laptop',
    basePrice: 999.99,
    taxRate: 0.08,
    categories: ['Electronics', 'Computers']
);

// Data Transfer Objects (DTOs)
class CreateUserRequest {
    public function __construct(
        public readonly string $name,
        public readonly string $email,
        public readonly string $password,
        public readonly ?string $phone = null,
        public readonly array $roles = ['user']
    ) {}
}

class UserResponse {
    public function __construct(
        public readonly int $id,
        public readonly string $name,
        public readonly string $email,
        public readonly \DateTime $createdAt,
        public readonly bool $isActive = true
    ) {}
    
    public function toArray(): array {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'created_at' => $this->createdAt->format('Y-m-d H:i:s'),
            'is_active' => $this->isActive
        ];
    }
}

// Configuration classes
class DatabaseConfig {
    public function __construct(
        public readonly string $host,
        public readonly string $database,
        public readonly string $username,
        private readonly string $password,
        public readonly int $port = 3306,
        public readonly string $charset = 'utf8mb4'
    ) {}
    
    public function getDsn(): string {
        return "mysql:host={$this->host};port={$this->port};dbname={$this->database};charset={$this->charset}";
    }
    
    public function getPassword(): string {
        return $this->password;
    }
}

// Value objects
class Money {
    public function __construct(
        private readonly int $amount,
        private readonly string $currency
    ) {
        if ($amount < 0) {
            throw new InvalidArgumentException('Amount cannot be negative');
        }
        
        if (strlen($currency) !== 3) {
            throw new InvalidArgumentException('Currency must be 3 characters');
        }
    }
    
    public function getAmount(): int {
        return $this->amount;
    }
    
    public function getCurrency(): string {
        return $this->currency;
    }
    
    public function format(): string {
        return number_format($this->amount / 100, 2) . ' ' . $this->currency;
    }
    
    public function add(Money $other): Money {
        if ($this->currency !== $other->currency) {
            throw new InvalidArgumentException('Cannot add different currencies');
        }
        
        return new Money($this->amount + $other->amount, $this->currency);
    }
}

$price = new Money(1999, 'USD'); // $19.99 USD
$tax = new Money(160, 'USD');    // $1.60 USD
$total = $price->add($tax);      // $21.59 USD

// API endpoint classes
class ApiEndpoint {
    public function __construct(
        public readonly string $method,
        public readonly string $path,
        public readonly string $controller,
        public readonly string $action,
        public readonly array $middleware = [],
        public readonly array $parameters = []
    ) {}
    
    public function matches(string $method, string $path): bool {
        return $this->method === strtoupper($method) && 
               $this->matchesPath($path);
    }
    
    private function matchesPath(string $path): bool {
        // Simple path matching logic
        return $this->path === $path;
    }
}

$endpoints = [
    new ApiEndpoint('GET', '/users', 'UserController', 'index'),
    new ApiEndpoint('POST', '/users', 'UserController', 'store', ['auth']),
    new ApiEndpoint('GET', '/users/{id}', 'UserController', 'show', parameters: ['id'])
];

// Event classes
class UserRegistered {
    public function __construct(
        public readonly int $userId,
        public readonly string $email,
        public readonly \DateTime $occurredAt
    ) {}
}

class OrderPlaced {
    public function __construct(
        public readonly string $orderId,
        public readonly int $userId,
        public readonly Money $total,
        public readonly array $items,
        public readonly \DateTime $placedAt
    ) {}
}

// Command pattern
abstract class Command {
    public function __construct(
        public readonly string $id,
        public readonly \DateTime $createdAt
    ) {}
    
    abstract public function execute(): void;
}

class SendEmailCommand extends Command {
    public function __construct(
        string $id,
        \DateTime $createdAt,
        public readonly string $to,
        public readonly string $subject,
        public readonly string $body
    ) {
        parent::__construct($id, $createdAt);
    }
    
    public function execute(): void {
        // Send email logic
        mail($this->to, $this->subject, $this->body);
    }
}

// Repository pattern with promoted properties
class UserRepository {
    public function __construct(
        private readonly \PDO $pdo,
        private readonly string $table = 'users'
    ) {}
    
    public function findById(int $id): ?User {
        $stmt = $this->pdo->prepare("SELECT * FROM {$this->table} WHERE id = ?");
        $stmt->execute([$id]);
        
        $data = $stmt->fetch(\PDO::FETCH_ASSOC);
        if (!$data) {
            return null;
        }
        
        return new User(
            name: $data['name'],
            email: $data['email'],
            age: $data['age'],
            active: (bool)$data['active']
        );
    }
}

// Service classes
class EmailService {
    public function __construct(
        private readonly string $smtpHost,
        private readonly int $smtpPort,
        private readonly string $username,
        private readonly string $password,
        private readonly bool $enableTls = true
    ) {}
    
    public function send(string $to, string $subject, string $body): bool {
        // Email sending logic using promoted properties
        return true;
    }
}

// Testing with promoted properties
class TestUser {
    public function __construct(
        public string $name = 'Test User',
        public string $email = 'test@example.com',
        public int $age = 25,
        public bool $active = true
    ) {}
    
    public static function make(array $overrides = []): self {
        return new self(
            name: $overrides['name'] ?? 'Test User',
            email: $overrides['email'] ?? 'test@example.com',
            age: $overrides['age'] ?? 25,
            active: $overrides['active'] ?? true
        );
    }
}

$testUser1 = TestUser::make();
$testUser2 = TestUser::make(['name' => 'Custom Name', 'age' => 30]);

// Validation classes
class ValidationRule {
    public function __construct(
        public readonly string $field,
        public readonly string $rule,
        public readonly mixed $value = null,
        public readonly string $message = ''
    ) {}
    
    public function validate(array $data): bool {
        $fieldValue = $data[$this->field] ?? null;
        
        return match ($this->rule) {
            'required' => !empty($fieldValue),
            'email' => filter_var($fieldValue, FILTER_VALIDATE_EMAIL) !== false,
            'min' => is_numeric($fieldValue) && $fieldValue >= $this->value,
            'max' => is_numeric($fieldValue) && $fieldValue <= $this->value,
            default => true
        };
    }
}

$rules = [
    new ValidationRule('email', 'required'),
    new ValidationRule('email', 'email'),
    new ValidationRule('age', 'min', 18),
    new ValidationRule('age', 'max', 100)
];

Explanation

Constructor property promotion automatically declares properties and assigns constructor parameters to them. Use visibility keywords (public, private, protected) before parameters.

This feature reduces boilerplate code significantly, especially for DTOs, value objects, and configuration classes. Mix promoted properties with regular properties when needed.

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Php