Skip to content

Constructors

A constructor defines what an Object needs at the moment it is created. PHP calls the special __construct() Method automatically when code uses new.

Typed Properties without initial values are uninitialized:

final class Order
{
public int $id;
public float $total;
}

Reading $order->id before assigning it throws an Error. It is not silently treated as null, because null is not an int.

A constructor makes the required starting State explicit:

final class Order
{
public int $id;
public float $total;
public function __construct(int $id, float $total)
{
$this->id = $id;
$this->total = $total;
}
}
$order = new Order(15, 500);

Calling new Order() without the required Arguments throws an ArgumentCountError; callers cannot receive an incompletely initialized Order.

An untyped declared Property behaves differently: without an explicit value, it starts as null. A typed Property—even a nullable one—must receive a value or an explicit = null default before it can be read.

PHP can declare and initialize Properties directly in the constructor:

final class Order
{
public function __construct(
public int $id,
public float $total,
) {}
}

The visibility keyword (public, protected, or private) makes the Parameter a promoted Property. Without visibility it remains an ordinary constructor Parameter.

A Parameter without a default is required. A Parameter with a default is optional and normally follows the required Parameters:

final class Order
{
public function __construct(
public int $id,
public float $total,
public ?string $note = null,
) {}
}

The default is used only when the caller omits that Argument. ?string allows string or null.

Named Arguments connect values to Parameter names:

$order = new Order(
total: 500,
id: 15,
);

Their order can differ from the declaration, and optional Arguments can be selected clearly. The trade-off is that Parameter names become part of the caller’s API: renaming $id to $orderId breaks callers that use id:.

An Object should receive a Dependency it requires to do its work rather than exist in an unusable, incomplete form:

final class CheckoutService
{
public function __construct(
private PaymentGateway $paymentGateway,
) {}
}

The Dependency is explicit, the Service does not create a concrete payment provider internally, and callers cannot construct the Service without a PaymentGateway. Dependency relationships are covered in depth later.

Sometimes a named creation operation communicates intent better than raw constructor Arguments:

$order = Order::createDraft();

Use a named constructor when it represents a genuine creation path with a meaningful name. Do not create a factory for every Class or every possible status. A transition such as activating an existing Order may belong in an instance Behavior instead.

final class Product
{
public function __construct(
public int $id,
public float $price,
public ?string $note = null,
) {}
}
$product = new Product(
price: 5.00,
id: 5,
);

The Product is created with id = 5, price = 5.0, and note = null.

  • Allowing an Object to exist before its required State is initialized.
  • Treating an uninitialized typed Property as if it were null.
  • Forgetting that a nullable typed Property still needs initialization.
  • Putting optional Parameters before required ones.
  • Renaming Parameters without considering Named-Argument callers.
  • Creating required Dependencies inside the dependent Class.
  • Introducing factories where a clear constructor is sufficient.

Constructors establish the Object’s required starting State and Dependencies. Property Promotion removes repetitive declarations, defaults model optional values, and Named Arguments improve readability at the cost of making Parameter names part of the public calling contract.

  1. Why is a typed Property without a value not automatically null?
  2. What turns a constructor Parameter into a promoted Property?
  3. What trade-off comes with Named Arguments?
  4. Why should a required Dependency be received through the constructor?