Why objects
So far, data has lived in arrays and logic in separate functions: a product is an array, and priceWithTax($product) is a function somewhere else. That works, but nothing stops code from putting a negative price into the array, and as programs grow it gets hard to know which functions belong to which data.
Object-oriented programming (OOP) bundles the data and the functions that work on it into one unit, an object. A product object knows its own name and price, and knows how to calculate its own price with tax. Most modern PHP, including Laravel, Symfony and WordPress's newer code, is written this way.
A class and an object
A class is the blueprint. It lists the properties (data) each object will have and the methods (functions) it can perform. new builds an object (an instance) from the class:
<?php
class Product
{
public string $name = '';
public float $price = 0.0;
public function priceWithTax(float $rate = 0.18): float
{
return round($this->price * (1 + $rate), 2);
}
}
$keyboard = new Product();
$keyboard->name = 'Mechanical Keyboard';
$keyboard->price = 89.5;
$mug = new Product();
$mug->name = 'Ceramic Mug Set';
$mug->price = 18.75;
echo $keyboard->name . ': ' . $keyboard->priceWithTax() . "\n";
echo $mug->name . ': ' . $mug->priceWithTax() . "\n";Mechanical Keyboard: 105.61
Ceramic Mug Set: 22.13Points to notice:
- Class names use PascalCase by convention:
Product,OrderLine,ShoppingCart. - The arrow
->reaches a property or method of an object:$keyboard->price,$keyboard->priceWithTax(). (No$before the property name.) - Inside a method,
$thismeans "the object this method was called on". The same method returns different results for$keyboardand$mugbecause$this->priceis different. - Properties can have types, just like function parameters. Assigning a string to
$pricewould throw aTypeError.
Methods that change the object
Methods can update the object's own properties. Here a cart keeps its lines and offers methods to add items and get the total:
<?php
class Cart
{
public array $lines = [];
public function add(string $product, float $price, int $qty = 1): void
{
$this->lines[] = ['product' => $product, 'price' => $price, 'qty' => $qty];
}
public function total(): float
{
$total = 0;
foreach ($this->lines as $line) {
$total += $line['price'] * $line['qty'];
}
return $total;
}
public function count(): int
{
return array_sum(array_column($this->lines, 'qty'));
}
}
$cart = new Cart();
$cart->add('Mechanical Keyboard', 89.5);
$cart->add('Ceramic Mug Set', 18.75, 2);
echo $cart->count() . " items, total " . $cart->total() . "\n";3 items, total 127Code that uses the cart does not need to know how lines are stored. It calls add(), count() and total(), and the class is free to change its insides later.
public and private
The cart above has a problem: $lines is public, so any code can do $cart->lines[] = 'nonsense'; and break total(). Visibility keywords control who can use each member:
public: anyone.private: only code inside this class.protected: this class and classes that extend it (see Inheritance and method overriding).
Make the data private and offer public methods that enforce the rules. That is called encapsulation:
<?php
class Product
{
private float $price = 0.0;
public function __construct(public string $name) {}
public function setPrice(float $price): void
{
if ($price < 0) {
throw new InvalidArgumentException("Price cannot be negative.");
}
$this->price = $price;
}
public function getPrice(): float
{
return $this->price;
}
}
$mat = new Product('Yoga Mat');
$mat->setPrice(24.99);
echo $mat->name . ' costs ' . $mat->getPrice() . "\n";
try {
$mat->setPrice(-5);
} catch (InvalidArgumentException $e) {
echo $e->getMessage() . "\n";
}Yoga Mat costs 24.99
Price cannot be negative.Trying $mat->price = -5; from outside would now fail with "Cannot access private property". The __construct(public string $name) {} line is a constructor, which runs when the object is created; the next lesson covers it properly.
Objects are shared, not copied
Arrays are copied when you assign them. Objects are not: the variable holds a handle to the object, and assigning it copies the handle. Two variables then point at the same object:
<?php
class Counter
{
public int $value = 0;
}
$a = new Counter();
$b = $a; // same object
$b->value = 10;
echo $a->value . "\n";
$c = clone $a; // a separate copy
$c->value = 99;
echo $a->value . " " . $c->value . "\n";10
10 99The same happens when you pass an object to a function: the function can change the object the caller holds. That is usually what you want with objects, but use clone when you need an independent copy.
Inspecting objects
var_dump() and print_r() work on objects and show their class and properties. instanceof checks whether a value is an object of a given class:
<?php
class Product
{
public function __construct(public string $name, private float $price) {}
}
$p = new Product('Water Bottle', 12.0);
var_dump($p instanceof Product);
print_r($p);
echo get_class($p) . "\n";bool(true)
Product Object
(
[name] => Water Bottle
[price:Product:private] => 12
)
ProductCommon mistakes
- Writing $this->$name instead of
$this->name. The extra$looks up a property whose name is the value of$name. - Forgetting $this inside a method:
return $price;uses a local variable, not the property. - Making everything public, so the class cannot protect its own rules.
- Expecting $b = $a to copy an object. Use
clone.
Exercise
Write a class BankAccount with a private balance (starting at 0), and public methods deposit(float $amount), withdraw(float $amount) and getBalance(). Deposits must be positive, and withdrawals may not exceed the balance; throw an exception otherwise. Deposit 500, withdraw 120, try to withdraw 1000, and print the balance.
Show one possible answer
<?php
class BankAccount
{
private float $balance = 0;
public function deposit(float $amount): void
{
if ($amount <= 0) {
throw new InvalidArgumentException("Deposit must be positive.");
}
$this->balance += $amount;
}
public function withdraw(float $amount): void
{
if ($amount > $this->balance) {
throw new RuntimeException("Insufficient funds.");
}
$this->balance -= $amount;
}
public function getBalance(): float
{
return $this->balance;
}
}
$account = new BankAccount();
$account->deposit(500);
$account->withdraw(120);
try {
$account->withdraw(1000);
} catch (RuntimeException $e) {
echo $e->getMessage() . "\n";
}
echo "Balance: " . $account->getBalance() . "\n";Insufficient funds.
Balance: 380Key points
- A class is a blueprint; an object is one thing built from it with new.
- Properties hold an object's data; methods are functions that belong to the object and use $this.
- public members can be used from anywhere; private ones only inside the class, which protects the object's rules.
- Objects are passed around by handle, so two variables can refer to the same object; use clone for a copy.