PHP Constructors: Understanding __construct() Magic Methods

PHP Constructors: Understanding __construct() Magic Methods

When you create an object in PHP, the constructor runs immediately:

class User {
    private string $name;
    
    public function __construct(string $name) {
        $this->name = $name;
    }
}

$user = new User("Alice");  // __construct() runs here

The __construct() method receives the arguments passed to new User() and initializes the object. Without a constructor, properties stay uninitialized until you set them manually.

Why Constructors Exist

Before constructors, you'd create an object, then set properties:

$user = new User();
$user->setName("Alice");
$user->setEmail("alice@example.com");

This works, but there's no guarantee the object is in a valid state between creation and configuration. If you forget to call setEmail(), the object is incomplete.

Constructors enforce initialization:

class User {
    public function __construct(
        private string $name,
        private string $email
    ) {}
}

$user = new User("Alice", "alice@example.com");

Now the object can't exist without a name and email. The constructor enforces the requirement.

Constructor Syntax

The basic form:

class ClassName {
    public function __construct() {
        // Initialization code
    }
}

Constructors don't have return types. They implicitly return the object being constructed. You can't write public function __construct(): ClassName.

The visibility is almost always public. Private constructors exist for singletons and static factories, but they're rare:

class Singleton {
    private static ?self $instance = null;
    
    private function __construct() {
        // Can't be called from outside
    }
    
    public static function getInstance(): self {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
}

Constructor Parameters

Pass data to constructors like any function:

class Product {
    private string $name;
    private float $price;
    
    public function __construct(string $name, float $price) {
        $this->name = $name;
        $this->price = $price;
    }
}

$product = new Product("Laptop", 999.99);

Default values work:

public function __construct(
    private string $name,
    private float $price = 0.0
) {}

$product = new Product("Free Item");  // Price defaults to 0.0

Named arguments (PHP 8.0+) make complex constructors clearer:

$user = new User(
    name: "Alice",
    email: "alice@example.com",
    role: "admin"
);

Constructor Property Promotion

PHP 8.0 introduced constructor property promotion. Instead of declaring properties and assigning them in the constructor:

class User {
    private string $name;
    private string $email;
    
    public function __construct(string $name, string $email) {
        $this->name = $name;
        $this->email = $email;
    }
}

You can declare and assign in one step:

class User {
    public function __construct(
        private string $name,
        private string $email
    ) {}
}

This generates the same bytecode but reduces boilerplate. The properties exist and are initialized automatically.

You can mix promoted and traditional parameters:

class User {
    private string $createdAt;
    
    public function __construct(
        private string $name,
        private string $email
    ) {
        $this->createdAt = date('Y-m-d H:i:s');
    }
}

Promoted properties ($name, $email) are set automatically. Traditional properties ($createdAt) are set in the constructor body.

Validation in Constructors

Constructors are a good place for validation:

class Email {
    public function __construct(private string $address) {
        if (!filter_var($address, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException("Invalid email: $address");
        }
    }
}

try {
    $email = new Email("not-an-email");
} catch (InvalidArgumentException $e) {
    echo $e->getMessage();
}

If validation fails, throw an exception. The object is never created. This prevents invalid objects from existing.

Calling Parent Constructors

If a class extends another, the parent constructor doesn't run automatically. Call it explicitly:

class Animal {
    public function __construct(private string $species) {}
}

class Dog extends Animal {
    public function __construct(
        string $species,
        private string $breed
    ) {
        parent::__construct($species);
    }
}

$dog = new Dog("Canine", "Labrador");

Without parent::__construct(), the parent's initialization doesn't happen. Properties set by the parent constructor remain uninitialized.

Constructor vs Static Factory Methods

Sometimes static factory methods are clearer than overloaded constructors:

class User {
    private function __construct(
        private string $name,
        private string $email,
        private string $role
    ) {}
    
    public static function createAdmin(string $name, string $email): self {
        return new self($name, $email, 'admin');
    }
    
    public static function createGuest(string $email): self {
        return new self('Guest', $email, 'guest');
    }
}

$admin = User::createAdmin("Alice", "alice@example.com");
$guest = User::createGuest("guest@example.com");

The private constructor prevents direct instantiation. Factory methods control how objects are created, making the code more readable.

Dependency Injection

Constructors are where dependencies are injected:

class UserRepository {
    public function __construct(private PDO $database) {}
    
    public function find(int $id): ?User {
        // Use $this->database
    }
}

$pdo = new PDO("mysql:host=localhost;dbname=app", "user", "pass");
$repo = new UserRepository($pdo);

The repository depends on a database connection. Instead of creating it inside the class, you pass it in. This makes testing easier—you can inject a mock database.

Frameworks like Laravel and Symfony do this automatically through service containers:

class UserController {
    public function __construct(private UserRepository $users) {}
    
    public function show(int $id) {
        $user = $this->users->find($id);
        // ...
    }
}

The framework resolves UserRepository and injects it when creating UserController.

When Not to Use Constructors

Avoid heavy work in constructors. Construction should be fast and deterministic. Don't:

  • Make network requests
  • Read large files
  • Perform expensive computations

If initialization is expensive, use lazy loading:

class ReportGenerator {
    private ?array $data = null;
    
    public function __construct(private string $reportPath) {}
    
    private function loadData(): void {
        if ($this->data === null) {
            $this->data = json_decode(
                file_get_contents($this->reportPath),
                true
            );
        }
    }
    
    public function generate(): string {
        $this->loadData();
        // Use $this->data
    }
}

The file is only read when generate() is called, not when the object is created.

Optional Parameters and Fluent Interfaces

For complex objects with many optional parameters, consider a builder pattern:

class EmailBuilder {
    private string $to;
    private string $subject;
    private string $body;
    private array $attachments = [];
    
    public function __construct() {}
    
    public function to(string $email): self {
        $this->to = $email;
        return $this;
    }
    
    public function subject(string $subject): self {
        $this->subject = $subject;
        return $this;
    }
    
    public function body(string $body): self {
        $this->body = $body;
        return $this;
    }
    
    public function attach(string $file): self {
        $this->attachments[] = $file;
        return $this;
    }
    
    public function send(): bool {
        // Send email using configured properties
        return true;
    }
}

$result = (new EmailBuilder())
    ->to("user@example.com")
    ->subject("Hello")
    ->body("Message content")
    ->attach("file.pdf")
    ->send();

The constructor is empty. Configuration happens through method calls.

Further Reading

The PHP manual's section on constructors and destructors covers the full syntax and behavior.

Constructor property promotion is detailed in the PHP 8.0 release notes.

For design patterns involving constructors, see Refactoring Guru's design patterns catalog, particularly the Factory Method and Builder patterns.

Constructors are where objects come to life in PHP.

Wear the code

Product mockup

function __construct() Developer T-Shirt (PHP Edition — Dark Mode)

£25.00

View product
Product mockup

function __construct() Developer T-Shirt (PHP Edition — Light Mode)

£25.00

View product

0 comments

Leave a comment

Please note, comments need to be approved before they are published.