Himanshu SharmaSoftware engineer
Let’s talk

PHP · Lesson 43 of 54

Constructors, typed and readonly properties

Initialise objects with constructors, shorten classes with constructor promotion, protect values with readonly, and use magic methods.

12 minObject-oriented PHPUpdated 22 October 2026

Awaiting technical review. This lesson is readable but not yet listed in search engines.

By the end of this lessonWrite classes whose objects are created in a valid state, with promoted and readonly properties.

Objects that start in a valid state

In the previous lesson a product was created empty and then filled in property by property. That allows half-built objects: a product with no name, or a price that was never set. A constructor fixes that. It is a method called __construct() that PHP runs automatically inside new, and its parameters are the values every object must have:

php
<?php
class Product
{
    public string $name;
    public float $price;

    public function __construct(string $name, float $price)
    {
        if ($price < 0) {
            throw new InvalidArgumentException("Price cannot be negative.");
        }
        $this->name = $name;
        $this->price = $price;
    }
}

$keyboard = new Product('Mechanical Keyboard', 89.5);
echo $keyboard->name . " " . $keyboard->price . "\n";
result
Mechanical Keyboard 89.5

Now it is impossible to create a product without a name and a price, or with a negative price. Leaving out an argument is an error:

php
<?php
class Product
{
    public function __construct(public string $name, public float $price) {}
}

$broken = new Product('Water Bottle');
result
Fatal error: Uncaught ArgumentCountError: Too few arguments to function Product::__construct(), 1 passed in /path/to/script.php on line 7 and exactly 2 expected in /path/to/script.php:4
Stack trace:
#0 /path/to/script.php(7): Product->__construct('Water Bottle')
#1 {main}
  thrown in /path/to/script.php on line 4

Methods whose names start with two underscores, such as __construct(), are called magic methods: PHP calls them for you at particular moments.

Constructor property promotion

The constructor above repeats every property three times: in the declaration, in the parameter list and in the assignment. PHP 8 lets you write a visibility keyword in front of a constructor parameter, which declares the property and assigns it in one go. This is called constructor property promotion:

php
<?php
class Customer
{
    public function __construct(
        public string $firstName,
        public string $lastName,
        private string $email,
        public string $country = 'India',
    ) {}

    public function fullName(): string
    {
        return "$this->firstName $this->lastName";
    }

    public function emailDomain(): string
    {
        return substr($this->email, strpos($this->email, '@') + 1);
    }
}

$asha = new Customer('Asha', 'Verma', 'asha@example.com');
$liam = new Customer(firstName: 'Liam', lastName: "O'Brien", email: 'liam@example.ie', country: 'Ireland');

echo $asha->fullName() . " from " . $asha->country . "\n";
echo $liam->fullName() . " (" . $liam->emailDomain() . ") from " . $liam->country . "\n";
result
Asha Verma from India
Liam O'Brien (example.ie) from Ireland

Promoted parameters can have defaults, and named arguments from Parameters, defaults, named arguments and types make long constructors readable. The constructor body can be empty, or it can still contain validation.

readonly properties

Some values should never change after an object is created: an order's ID, the date it was placed, the currency of a price. Since PHP 8.1, a readonly property can be assigned exactly once, from inside the class, and is then fixed:

php
<?php
class Order
{
    public function __construct(
        public readonly int $id,
        public readonly string $placedOn,
    ) {}
}

$order = new Order(1042, '2024-06-03');
echo "Order {$order->id} placed on {$order->placedOn}\n";

try {
    $order->id = 9999;
} catch (Error $e) {
    echo get_class($e) . ": " . $e->getMessage() . "\n";
}
result
Order 1042 placed on 2024-06-03
Error: Cannot modify readonly property Order::$id

Readonly properties can be public safely, because nobody can change them, which saves writing getter methods. A readonly property must have a type. PHP 8.2 added readonly classes, readonly class Money { ... }, which make every property readonly.

Immutable value objects

A value object is a small object that represents a value, such as an amount of money, a date range or an email address, and that never changes. To "change" it, a method returns a new object. You have used one already: DateTimeImmutable in Dates and times. Here is a simple money class built the same way:

php
<?php
final class Money
{
    public function __construct(
        public readonly int $paise,
        public readonly string $currency = 'INR',
    ) {
        if ($paise < 0) {
            throw new InvalidArgumentException("Amount cannot be negative.");
        }
    }

    public function add(Money $other): Money
    {
        if ($other->currency !== $this->currency) {
            throw new InvalidArgumentException("Currencies differ.");
        }
        return new Money($this->paise + $other->paise, $this->currency);
    }

    public function times(int $quantity): Money
    {
        return new Money($this->paise * $quantity, $this->currency);
    }

    public function __toString(): string
    {
        return $this->currency . ' ' . number_format($this->paise / 100, 2);
    }
}

$keyboard = new Money(8950);
$mugs = (new Money(1875))->times(2);
$total = $keyboard->add($mugs);

echo "Keyboard: $keyboard\n";
echo "Mugs: $mugs\n";
echo "Total: $total\n";
result
Keyboard: INR 89.50
Mugs: INR 37.50
Total: INR 127.00

Storing paise as an integer avoids the float problems from Integers, floats and maths. final means no other class may extend this one. __toString() is the magic method PHP calls when an object is used as a string, which is why "Total: $total" works.

Other magic methods

A few more magic methods are worth recognising:

  • __toString(): the object as a string, shown above.
  • __get($name) / __set($name, $value): called when code reads or writes a property that does not exist or is not accessible.
  • __call($name, $args): called for a method that does not exist. Frameworks use these for "magic" features; in your own code, prefer real properties and methods, which editors understand.
  • __clone(): runs on the copy after clone, to adjust it.
  • __destruct(): runs when the object is destroyed, rarely needed in PHP.

Common mistakes

  • Doing heavy work in a constructor, such as connecting to a database or sending email. Keep constructors to assigning and checking values.
  • Forgetting the visibility keyword on a parameter, so it is not promoted and no property is created.
  • Trying to change a readonly property after construction. Create a new object instead.
  • Using floats for money in value objects.

Exercise

Write a readonly class Temperature with a celsius float property, a method toFahrenheit(): float, a method warmer(float $degrees): Temperature that returns a new object, and __toString() that prints like 25.0°C. Create 25 degrees, make it 3 degrees warmer, and print both temperatures and the warmer one in Fahrenheit.

Show one possible answer
php
<?php
readonly class Temperature
{
    public function __construct(public float $celsius) {}

    public function toFahrenheit(): float
    {
        return $this->celsius * 9 / 5 + 32;
    }

    public function warmer(float $degrees): Temperature
    {
        return new Temperature($this->celsius + $degrees);
    }

    public function __toString(): string
    {
        return number_format($this->celsius, 1) . '°C';
    }
}

$today = new Temperature(25);
$tomorrow = $today->warmer(3);

echo "Today: $today, tomorrow: $tomorrow\n";
echo "Tomorrow in Fahrenheit: " . $tomorrow->toFahrenheit() . "\n";
result
Today: 25.0°C, tomorrow: 28.0°C
Tomorrow in Fahrenheit: 82.4

Key points

  • __construct() runs when an object is created; use it to require and check the starting values.
  • Constructor promotion, __construct(public string $name), declares and assigns a property in one place.
  • readonly properties can be set once, inside the class, and never changed afterwards.
  • Immutable objects return changed copies with 'with' methods instead of changing themselves.

Check yourself

1. When does __construct() run?
2. What does function __construct(private float $price) {} do?
3. What happens when code assigns to a readonly property that is already set?
4. What is the purpose of __toString()?