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
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";Mechanical Keyboard 89.5Now 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
class Product
{
public function __construct(public string $name, public float $price) {}
}
$broken = new Product('Water Bottle');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 4Methods 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
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";Asha Verma from India
Liam O'Brien (example.ie) from IrelandPromoted 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
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";
}Order 1042 placed on 2024-06-03
Error: Cannot modify readonly property Order::$idReadonly 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
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";Keyboard: INR 89.50
Mugs: INR 37.50
Total: INR 127.00Storing 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 afterclone, 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
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";Today: 25.0°C, tomorrow: 28.0°C
Tomorrow in Fahrenheit: 82.4Key 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.