objects namespaces and application architecture
Constructors and Destructors
A constructor runs when an object is created. Its job is to put the object into a usable state immediately.
A destructor runs when PHP destroys an object. Destructors exist, but they are much less common in application code. Important business work should not depend on a destructor because timing can be hard to reason about.
Use constructors for required state
Without a constructor, code can create an object and forget to fill important properties. A constructor makes required values explicit.
<?php
declare(strict_types=1);
class Product
{
public function __construct(
public string $sku,
public string $name,
public int $pricePennies,
) {
}
}
$product = new Product('KB-101', 'Keyboard', 2499);
echo $product->sku . ' / ' . $product->name . PHP_EOL;
// Prints:
// KB-101 / Keyboard
This uses constructor property promotion, which is common in modern PHP. The constructor parameters become properties automatically.
Validate constructor input
A constructor can protect the object from invalid starting values.
<?php
declare(strict_types=1);
class OrderLine
{
public function __construct(
public string $sku,
public int $unitPricePennies,
public int $quantity,
) {
if (trim($sku) === '') {
throw new InvalidArgumentException('SKU is required.');
}
if ($unitPricePennies < 0) {
throw new InvalidArgumentException('Unit price must not be negative.');
}
if ($quantity < 1) {
throw new InvalidArgumentException('Quantity must be at least 1.');
}
}
public function lineTotalPennies(): int
{
return $this->unitPricePennies * $this->quantity;
}
}
$line = new OrderLine('KB-101', 2499, 2);
echo $line->lineTotalPennies() . PHP_EOL;
// Prints:
// 4998
After construction, the rest of the application can trust that an OrderLine has the basic data it needs.
Constructors should not do too much
Constructors should usually assign and validate state. Be careful when they send emails, write files, call APIs, or run database queries.
<?php
declare(strict_types=1);
class WelcomeEmail
{
public function __construct(
public string $recipient,
public string $subject,
) {
if (filter_var($recipient, FILTER_VALIDATE_EMAIL) === false) {
throw new InvalidArgumentException('Recipient email is invalid.');
}
}
}
$email = new WelcomeEmail('nia@example.com', 'Welcome');
echo $email->subject . PHP_EOL;
// Prints:
// Welcome
Creating the message object is separate from sending it. That keeps object creation predictable and testable.
Use named constructors when creation has meaning
Sometimes one class has more than one useful way to create an object. A named constructor is a static method that explains that creation path.
<?php
declare(strict_types=1);
class Money
{
public function __construct(public int $pennies)
{
if ($pennies < 0) {
throw new InvalidArgumentException('Money cannot be negative here.');
}
}
public static function poundsAndPennies(int $pounds, int $pennies): self
{
return new self(($pounds * 100) + $pennies);
}
}
$money = Money::poundsAndPennies(24, 99);
echo $money->pennies . PHP_EOL;
// Prints:
// 2499
Named constructors can make calling code more expressive than a long list of primitive constructor arguments.
Destructors are for cleanup, not business rules
Destructors can release resources, but most PHP application code should close files, commit transactions, and send messages explicitly.
<?php
declare(strict_types=1);
class TemporaryNote
{
public function __construct(public string $path)
{
file_put_contents($this->path, "temporary\n");
}
public function __destruct()
{
if (is_file($this->path)) {
unlink($this->path);
}
}
}
$path = tempnam(sys_get_temp_dir(), 'note_');
$note = new TemporaryNote($path);
echo is_file($path) ? 'exists' : 'missing';
echo PHP_EOL;
unset($note);
echo is_file($path) ? 'exists' : 'missing';
echo PHP_EOL;
// Prints:
// exists
// missing
This is a small cleanup example. Do not hide important application behaviour in __destruct().
What to remember
Constructors make required object state explicit and should leave objects usable immediately. Validate invariants there, keep side effects modest, use named constructors when creation needs a clearer name, and reserve destructors for limited cleanup rather than business workflows.
Practice
Task: Create a valid order line
Write an OrderLine class that cannot be created with invalid starting data.
Requirements
- Use
declare(strict_types=1);. - Use a constructor.
- Require a non-empty SKU.
- Require a non-negative unit price in pennies.
- Require a quantity of at least
1. - Add a method that returns the line total in pennies.
- Print one valid line total.
- Show one invalid quantity case by catching the exception.
- Include the expected output as comments in the same PHP code block.
The point is to make invalid object state fail at construction time.
Show solution
<?php
declare(strict_types=1);
class OrderLine
{
public function __construct(
public string $sku,
public int $unitPricePennies,
public int $quantity,
) {
if (trim($sku) === '') {
throw new InvalidArgumentException('SKU is required.');
}
if ($unitPricePennies < 0) {
throw new InvalidArgumentException('Unit price must not be negative.');
}
if ($quantity < 1) {
throw new InvalidArgumentException('Quantity must be at least 1.');
}
}
public function lineTotalPennies(): int
{
return $this->unitPricePennies * $this->quantity;
}
}
$line = new OrderLine('KB-101', 2499, 2);
echo $line->lineTotalPennies() . PHP_EOL;
try {
new OrderLine('KB-101', 2499, 0);
} catch (InvalidArgumentException $exception) {
echo $exception->getMessage() . PHP_EOL;
}
// Prints:
// 4998
// Quantity must be at least 1.
The constructor makes the required state explicit. Code that receives an OrderLine object can rely on the quantity and price rules having already been checked.