Defining a class and creating objects

The ideas behind object-oriented programming — a class as a blueprint, an object as one thing built from it, encapsulation and inheritance — are explained in Programming Concepts. This page shows how PHP writes them down. PHP's object model dates from PHP 5 (2004) and has grown a lot in PHP 8; most WordPress plugins, Laravel and Symfony code is written with it.

A class lists properties (the data each object holds, preferably with a type) and methods (functions that belong to the class). new creates an object, the arrow -> reaches a property or method, and inside a method $this is the object the method was called on.

<?php
class MeterReading
{
    public string $meterId;
    public float $kwh;

    public function describe(): string
    {
        return "Meter {$this->meterId}: {$this->kwh} kWh";
    }
}

$reading = new MeterReading();
$reading->meterId = "M-1042";
$reading->kwh = 412.5;

echo $reading->describe(), "\n";
echo get_class($reading), "\n";
var_dump($reading instanceof MeterReading);
Meter M-1042: 412.5 kWh
MeterReading
bool(true)

Note that the property name has no $ after the arrow: $reading->kwh, not $reading->$kwh (that second form means "the property whose name is stored in the variable $kwh"). instanceof checks whether an object is of a class, a subclass of it, or an interface it implements.

Typed properties

A property with a type, such as public float $kwh;, only accepts values of that type (with the usual conversions described on Variables, Types and Type Juggling). A typed property without a default starts uninitialized, which is not the same as null: reading it before anything was assigned throws an error.

<?php
class MeterReading
{
    public string $meterId = "unknown";
    public float $kwh;
}

$reading = new MeterReading();
echo $reading->meterId, "\n";
echo $reading->kwh, "\n";
unknown
Fatal error: Uncaught Error: Typed property MeterReading::$kwh must not be accessed before initialization in example.php:10

The usual fix is a constructor that fills every property, which is the next section.

Objects are handles

A variable does not contain the object itself but a handle to it. Assigning an object to another variable, or passing it to a function, copies the handle — both names then refer to the same object. Arrays behave the other way: assigning an array copies it. To get an independent copy of an object, use clone.

<?php
class MeterReading
{
    public function __construct(public float $kwh) {}
}

$first = new MeterReading(412.5);
$same = $first;          // copies the handle: one object
$same->kwh = 500.0;
echo $first->kwh, "\n";

$copy = clone $first;    // a second, independent object
$copy->kwh = 600.0;
echo $first->kwh, " ", $copy->kwh, "\n";

$list = [1, 2];
$other = $list;          // arrays are copied on assignment
$other[] = 3;
echo count($list), " ", count($other), "\n";
500
500 600
2 3

clone makes a shallow copy: properties that hold other objects still point to the same objects. A class can define a __clone() method to clone those as well.

Constructors

A constructor is the method PHP calls automatically when new creates an object. In PHP it is always named __construct; the arguments given to new are passed to it.

<?php
class Customer
{
    public string $name;
    public string $city;

    public function __construct(string $name, string $city = "Deventer")
    {
        $this->name = $name;
        $this->city = $city;
    }
}

$lotte = new Customer("Lotte Visser");
$pieter = new Customer(city: "Zwolle", name: "Pieter de Graaf");

echo "{$lotte->name} ({$lotte->city})\n";
echo "{$pieter->name} ({$pieter->city})\n";
Lotte Visser (Deventer)
Pieter de Graaf (Zwolle)

Because a constructor is a function, everything from Functions applies: type declarations, default values and named arguments, as in the second new call. Methods whose names start with two underscores, such as __construct, __toString and __clone, are PHP's magic methods: PHP calls them for you at specific moments.

Constructor property promotion

The constructor above names each property three times: in the declaration, in the parameter list and in the assignment. Constructor property promotion (PHP 8.0) removes that repetition: a constructor parameter with a visibility keyword in front of it is declared as a property and assigned automatically.

<?php
class Customer
{
    public function __construct(
        public string $name,
        public string $city = "Deventer",
        private int $id = 0,
    ) {}

    public function label(): string
    {
        return sprintf("C-%04d %s, %s", $this->id, $this->name, $this->city);
    }
}

$customer = new Customer("Lotte Visser", id: 1042);
echo $customer->label(), "\n";
C-1042 Lotte Visser, Deventer

The empty body {} is fine; you can still add code to it (for example to validate a value), and you can mix promoted and ordinary parameters. A trailing comma after the last parameter is allowed and makes diffs cleaner when you add one. This is the style you will see in most modern PHP code.

Visibility

Visibility decides which code may use a property, method or constant. PHP has three levels and enforces them at run time:

Keyword Accessible from
public everywhere (the default for methods without a keyword)
protected the class itself and classes that extend it
private only the class that declares it

Making data private and offering public methods to change it is encapsulation in practice: every change goes through code that can check it.

<?php
class PrepaidAccount
{
    private float $balance = 0.0;

    public function topUp(float $euros): void
    {
        if ($euros <= 0) {
            throw new InvalidArgumentException("Top-up must be positive");
        }
        $this->balance += $euros;
    }

    public function balance(): float
    {
        return $this->balance;
    }
}

$account = new PrepaidAccount();
$account->topUp(25);
echo $account->balance(), "\n";
$account->balance = 1000;
25
Fatal error: Uncaught Error: Cannot access private property PrepaidAccount::$balance in example.php:23

A property and a method may share a name ($balance and balance()); PHP tells them apart by the parentheses. The last line is stopped with an Error, which you can read about on Errors and Exceptions.

Readonly properties

A readonly property (PHP 8.1) can be assigned exactly once, from inside the class — usually in the constructor — and never changed afterward. It must have a type. Objects whose properties are all readonly are immutable: once created, they cannot change, so they can be passed around without anyone altering them behind your back. A meter reading is a good candidate: it records what a meter showed at one moment.

<?php
final class MeterReading
{
    public function __construct(
        public readonly string $meterId,
        public readonly float $kwh,
    ) {}

    public function withKwh(float $kwh): static
    {
        return new static($this->meterId, $kwh);
    }
}

$reading = new MeterReading("M-1042", 412.5);
$corrected = $reading->withKwh(415.8);
echo $reading->kwh, " ", $corrected->kwh, "\n";

$reading->kwh = 0.0;
412.5 415.8
Fatal error: Uncaught Error: Cannot modify readonly property MeterReading::$kwh in example.php:19

Instead of changing the object, withKwh() returns a new one: the "with-er" pattern used by immutable objects such as DateTimeImmutable. PHP 8.2 added readonly class, which makes every property of the class readonly.

Static members and class constants

Some data belongs to the class rather than to one object: a VAT rate that is the same for every tariff, or a counter of how many objects were made. Class constants (const) and static properties and methods are reached through the class with ::, the scope resolution operator. Inside the class, self:: refers to the class itself. A static method has no $this, because it is not called on an object.

<?php
class Tariff
{
    public const VAT_RATE = 0.21;
    private static int $created = 0;

    public function __construct(public float $pricePerKwh)
    {
        self::$created++;
    }

    public static function created(): int
    {
        return self::$created;
    }

    public function priceWithVat(float $kwh): float
    {
        return round($kwh * $this->pricePerKwh * (1 + self::VAT_RATE), 2);
    }
}

$day = new Tariff(0.25);
$night = new Tariff(0.18);

echo Tariff::created(), "\n";
echo Tariff::VAT_RATE, "\n";
echo $day->priceWithVat(100), "\n";
echo $night->priceWithVat(100), "\n";
2
0.21
30.25
21.78

Static properties are shared by the whole program for the duration of one request, so in a web application a static counter starts at zero again on the next request (see What Is PHP? for why every request starts fresh). Use them sparingly: shared, changeable state makes code hard to test. Constants are always safe. Inside PHP the :: token is called T_PAAMAYIM_NEKUDOTAYIM (Hebrew for "double colon"); PHP 7 and older printed that name in parse errors, while PHP 8 shows unexpected token "::".

Inheritance and abstract classes

A class can extends exactly one parent class. It inherits the parent's public and protected members, can add its own, and can override methods. parent:: calls the parent's version of a method, for example the parent constructor.

An abstract class cannot be instantiated; it exists to be extended. Its abstract methods have no body, and every concrete subclass must implement them.

<?php
abstract class Notification
{
    public function __construct(protected string $customer) {}

    abstract protected function channel(): string;

    public function send(string $message): string
    {
        return "[{$this->channel()}] To {$this->customer}: {$message}";
    }
}

class EmailNotification extends Notification
{
    protected function channel(): string
    {
        return "email";
    }
}

class SmsNotification extends Notification
{
    public function __construct(string $customer, private int $maxLength = 24)
    {
        parent::__construct($customer);
    }

    protected function channel(): string
    {
        return "sms";
    }

    public function send(string $message): string
    {
        return parent::send(substr($message, 0, $this->maxLength));
    }
}

$message = "Planned outage on 12 October, 09:00-11:00";
$notifications = [
    new EmailNotification("Lotte Visser"),
    new SmsNotification("Pieter de Graaf"),
];
foreach ($notifications as $notification) {
    echo $notification->send($message), "\n";
}
[email] To Lotte Visser: Planned outage on 12 October, 09:00-11:00
[sms] To Pieter de Graaf: Planned outage on 12 Oct

send() is written once in the parent but calls $this->channel(), which runs the subclass's version: polymorphism. $customer is protected, so subclasses can use it while outside code cannot. new Notification("…") would throw Error: Cannot instantiate abstract class Notification.

The keyword final works the other way round: a final class cannot be extended, and a final method cannot be overridden. Many teams make classes final by default and open them up only when extension is really needed.

Interfaces

An interface lists methods a class promises to provide, without any code. A class can implements several interfaces, and a function can accept "anything that implements this interface" as its parameter type. That lets unrelated classes be used in the same place.

<?php
interface Billable
{
    public function amountDue(): float;
}

class Invoice implements Billable
{
    public function __construct(private float $amount) {}

    public function amountDue(): float
    {
        return $this->amount;
    }
}

class OutageCompensation implements Billable
{
    public function __construct(private int $hours) {}

    public function amountDue(): float
    {
        return -2.50 * $this->hours;   // a credit on the bill
    }
}

function total(Billable ...$items): float
{
    return array_sum(array_map(fn (Billable $item) => $item->amountDue(), $items));
}

echo total(new Invoice(84.20), new OutageCompensation(6)), "\n";
var_dump(new Invoice(1.0) instanceof Billable);
69.2
bool(true)

PHP has several built-in interfaces. Countable makes count($object) work, IteratorAggregate makes an object usable in foreach, and any class with a __toString() method automatically implements Stringable.

Traits

Because a class has only one parent, inheritance cannot share code between classes that are otherwise unrelated. A trait is a block of methods (and properties) that PHP copies into every class that says use TraitName; — as if you had pasted the code there. Traits cannot be instantiated and are not types: you cannot use a trait name as a parameter type or with instanceof.

<?php
trait RecordsHistory
{
    private array $history = [];

    public function record(string $event): void
    {
        $this->history[] = $event;
    }

    public function history(): string
    {
        return implode(" -> ", $this->history);
    }
}

class OutageTicket
{
    use RecordsHistory;

    public function __construct(public string $id)
    {
        $this->record("opened");
    }
}

class Customer
{
    use RecordsHistory;
}

$ticket = new OutageTicket("OUT-0311");
$ticket->record("crew assigned");
$ticket->record("resolved");
echo $ticket->history(), "\n";

$customer = new Customer();
$customer->record("moved to Zwolle");
echo $customer->history(), "\n";
opened -> crew assigned -> resolved
moved to Zwolle

Traits are handy for small, self-contained behavior such as logging or timestamps. For anything larger, prefer composition — an object that holds another object — because code pasted in from several traits is hard to follow.

Enums

A PHP enum (enumeration, PHP 8.1) is a type with a fixed set of named cases. A ticket status, a meter type or a tariff period can only be one of a few values; an enum makes any other value impossible, where a plain string such as "in_progres" (typo) would slip through.

A pure enum has only cases (enum Phase { case L1; case L2; case L3; }). A backed enum gives each case a string or int value, which is what you store in a database or send as JSON. from() turns such a value back into a case and throws a ValueError for an unknown value; tryFrom() returns null instead.

<?php
enum TicketStatus: string
{
    case Open = 'open';
    case InProgress = 'in_progress';
    case Resolved = 'resolved';

    public function label(): string
    {
        return match ($this) {
            TicketStatus::Open => 'Open',
            TicketStatus::InProgress => 'In progress',
            TicketStatus::Resolved => 'Resolved',
        };
    }
}

$status = TicketStatus::from('in_progress');   // e.g. read from the database
echo $status->name, " | ", $status->value, " | ", $status->label(), "\n";
var_dump($status === TicketStatus::InProgress);
var_dump(TicketStatus::tryFrom('closed'));
echo count(TicketStatus::cases()), "\n";
InProgress | in_progress | In progress
bool(true)
NULL
3

Each case is a single object, so === compares cases reliably. Enums can have methods and implement interfaces, but no properties of their own. The match expression in label() pairs well with enums: if a case is added later and not handled, match throws an error instead of silently doing nothing. TicketStatus::from('closed') would throw ValueError: "closed" is not a valid backing value for enum TicketStatus.

Namespaces

Large applications and the packages they install contain thousands of classes, and two of them are bound to be called Invoice or Logger. A PHP namespace is a named container for classes, interfaces, traits, enums, functions and constants, so that Kestrel\Billing\Invoice and Acme\Shop\Invoice can exist side by side. The separator is a backslash.

<?php
namespace Kestrel\Billing;

class Invoice
{
    public function __construct(public string $number) {}
}

function vat(float $amount): float
{
    return round($amount * 0.21, 2);
}

$invoice = new Invoice("INV-2026-0042");
echo get_class($invoice), "\n";
echo Invoice::class, "\n";
echo vat(84.20), "\n";
echo \strlen($invoice->number), "\n";
Kestrel\Billing\Invoice
Kestrel\Billing\Invoice
17.68
13

The namespace line must be the first statement in the file. Inside the namespace, a short name such as Invoice means Kestrel\Billing\Invoice. Invoice::class gives the full name as a string, which is useful in configuration and error messages. A leading backslash, as in \strlen, means "the global one"; for functions and constants PHP falls back to the global name anyway, but for classes it does not, so built-in classes need either the backslash (new \DateTimeImmutable()) or a use line.

In another file, use imports a name so you can write it short. It does not load anything; it only sets up an alias:

<?php
namespace Kestrel\Portal;

use DateTimeImmutable;
use Kestrel\Billing\Invoice;
use Kestrel\Billing\Invoice as BillingInvoice;   // alias, if names clash

$invoice = new Invoice("INV-2026-0043");
$issued = new DateTimeImmutable("2026-10-02");

By convention each class lives in its own file, and the namespace mirrors the folder structure: Kestrel\Billing\Invoice in src/Billing/Invoice.php. Composer's PSR-4 autoloader uses that convention to load each class file the first time the class is used, so you never write a long list of require lines — see Composer and Packages.