Classes and Objects

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against PHP 8.5, checked on 2026-10-05. It covers the basics of PHP’s object model; visibility and immutability, property hooks, inheritance and interfaces, traits, enums, magic methods and object lifecycle have pages of their own, linked below. The running scenario’s classes look like this:

classDiagram class Author { +string name +?string country } class Book { +string isbn +string title +Author author +int year +Format format +label() string } class Format { <<enumeration>> Hardcover Paperback Ebook +loanDays() int } class Member { +string id +string name } class Loan { +Book book +Member member +DateTimeImmutable borrowedAt +dueAt() DateTimeImmutable +isOverdue(DateTimeImmutable now) bool } class Catalogue { -array books +add(Book book) void +find(string isbn) ?Book +count() int } Book --> Author : written by Book --> Format Loan --> Book Loan --> Member Catalogue o-- Book : holds

Declaring a class and creating objects

A class declares properties (state), methods (behaviour) and constants. new creates an instance; inside methods $this refers to it, and -> accesses its members. ClassName::class (and $object::class since 8.0) gives the fully qualified class name as a string.

<?php

declare(strict_types=1);

namespace Library;

final class Author
{
    public string $name;
    public ?string $country = null;

    public function __construct(string $name, ?string $country = null)
    {
        $this->name = $name;
        $this->country = $country;
    }

    public function label(): string
    {
        return $this->country === null ? $this->name : "{$this->name} ({$this->country})";
    }
}

$lem = new Author('Stanisław Lem', 'PL');
echo $lem->label(), "\n";                     // Stanisław Lem (PL)
echo Author::class, ' ', $lem::class, "\n";   // Library\Author Library\Author
var_dump($lem instanceof Author);             // bool(true)

// PHP 8.4: call a method on a new object without wrapping parentheses
echo new Author('Octavia E. Butler', 'US')->label(), "\n";
// before 8.4: (new Author('Octavia E. Butler', 'US'))->label()

Classes are declared once per request and are case-insensitive (but autoloaders map names to case-sensitive file paths, so always use the declared case). Mark classes final unless they are designed for extension; see Inheritance, Interfaces and Abstract Classes.

Source: The Basics

Properties

Properties are declared with a visibility (public, protected, private, covered in Visibility, Readonly and Asymmetric Visibility), an optional type (7.4) and an optional default, which must be a constant expression.

  • A typed property without a default is uninitialised until assigned: reading it throws an Error.

  • An untyped property defaults to null.

  • static properties belong to the class, not to instances.

  • Since 8.4, properties can have hooks (get/set), described in Property Hooks.

<?php

declare(strict_types=1);

final class Book
{
    public string $isbn;                 // uninitialised until the constructor sets it
    public string $title = 'Untitled';   // default value
    public array $tags = [];
    public ?int $year = null;            // nullable with an explicit null default
}

$b = new Book();
var_dump(isset($b->isbn), $b->title, $b->year);   // bool(false) "Untitled" NULL

try {
    echo $b->isbn;
} catch (Error $e) {
    echo $e->getMessage(), "\n";   // Typed property Book::$isbn must not be accessed before initialization
}

$b->isbn = '978-0-00-000001-1';
echo $b->isbn, "\n";

Source: Properties

Dynamic properties are deprecated

Assigning to a property that was not declared creates a dynamic property. Since PHP 8.2 this emits a deprecation, because it is almost always a typo. Classes that genuinely need dynamic properties declare #[\AllowDynamicProperties]; stdClass and classes using __get()/__set() are unaffected.

<?php

declare(strict_types=1);

final class Member { public string $name = 'Ada'; }

$m = new Member();
$m->nmae = 'typo';      // Deprecated: Creation of dynamic property Member::$nmae is deprecated

#[\AllowDynamicProperties]
final class LegacyRecord {}

$r = new LegacyRecord();
$r->anything = 1;       // allowed, no deprecation
var_dump($r->anything);

Source: Properties (dynamic properties) and AllowDynamicProperties

Class constants

Class constants are declared with const, have a visibility (7.1) and, since 8.1, can be final. Since 8.3 they can be typed, and since 8.3 their name can be computed with the dynamic fetch syntax ClassName::{$name}. Constants are accessed through the class (Loan::MAX_DAYS), self:: or static::, and their values are constant expressions, including enum cases and, since 8.5, closures.

<?php

declare(strict_types=1);

class LoanPolicy
{
    public const int MAX_DAYS = 21;                 // 8.3: typed constant
    final public const string CURRENCY = 'EUR';     // 8.1: cannot be overridden
    protected const array FINES = ['day' => 0.25, 'max' => 10.0];

    public static function maxFine(): float
    {
        return static::FINES['max'];
    }
}

final class StudentPolicy extends LoanPolicy
{
    public const int MAX_DAYS = 28;                 // overriding a non-final constant
}

echo LoanPolicy::MAX_DAYS, ' ', StudentPolicy::MAX_DAYS, ' ', StudentPolicy::CURRENCY, "\n";   // 21 28 EUR

$which = 'MAX_DAYS';
echo StudentPolicy::{$which}, "\n";                 // 28 -- dynamic constant fetch, 8.3
echo LoanPolicy::maxFine(), "\n";                   // 10

::class is a special constant resolved at compile time, and self::class/static::class give the current and the late-bound class name.

Source: Class Constants

Constructors, promotion and destructors

__construct() runs when the object is created; __destruct() runs when the last reference to it disappears (or at script end). Constructor promotion (8.0) declares and assigns properties directly from constructor parameters, which removes most constructor boilerplate. Promoted properties can be readonly, have defaults and attributes, and can be mixed with ordinary parameters.

<?php

declare(strict_types=1);

final class Loan
{
    public function __construct(
        public readonly string $isbn,
        public readonly string $memberId,
        public readonly DateTimeImmutable $borrowedAt = new DateTimeImmutable('2026-10-05'),   // 8.1: new in initializer
        int $days = 21,                                          // a plain, non-promoted parameter
    ) {
        $this->dueAt = $borrowedAt->modify("+{$days} days");
    }

    public readonly DateTimeImmutable $dueAt;

    public function __destruct()
    {
        echo "Loan {$this->isbn} released\n";
    }
}

$loan = new Loan('978-0-00-000001-1', 'M-17');
echo $loan->dueAt->format('Y-m-d'), "\n";     // 2026-10-26
unset($loan);                                  // Loan 978-0-00-000001-1 released
echo "after unset\n";

Destructors are rarely needed in PHP: memory, files and connections are released automatically. Do not rely on the exact moment a destructor runs, and never throw from one during shutdown. Named constructors (public static function fromArray(array $row): self) are a common complement to __construct() for alternative ways of creating objects.

Static members and the scope-resolution operator

static properties and methods belong to the class. They are accessed with the scope-resolution operator :: (called Paamayim Nekudotayim in error messages), through the class name, self:: (the class where the code is written), parent:: or static:: (the class used at runtime, see Cloning, Late Static Binding and Object Lifecycle). Static methods have no $this.

<?php

declare(strict_types=1);

final class Isbn
{
    private static int $validated = 0;

    public static function isValid13(string $isbn): bool
    {
        self::$validated++;
        $digits = str_replace('-', '', $isbn);
        if (!preg_match('/^\d{13}$/', $digits)) {
            return false;
        }
        $sum = 0;
        foreach (str_split($digits) as $i => $d) {
            $sum += (int) $d * ($i % 2 === 0 ? 1 : 3);
        }
        return $sum % 10 === 0;
    }

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

var_dump(Isbn::isValid13('978-0-306-40615-7'));   // bool(true)  -- a valid check digit
var_dump(Isbn::isValid13('978-0-306-40615-8'));   // bool(false)
echo Isbn::validatedCount(), "\n";                // 2

Static state is global state: it survives for the whole request (and for the whole worker lifetime in FrankenPHP worker mode or Octane), and it complicates testing. Use static methods for pure helpers and named constructors; keep mutable state in objects.

Objects are handles

A variable does not contain an object but a handle to it. Assigning or passing an object copies the handle: both names refer to the same object. clone creates a copy (shallow by default, see Cloning, Late Static Binding and Object Lifecycle).

<?php

declare(strict_types=1);

final class Shelf { public array $titles = []; }

function addTitle(Shelf $shelf, string $title): void
{
    $shelf->titles[] = $title;      // modifies the caller's object: same handle
}

$a = new Shelf();
$b = $a;                            // same object
addTitle($b, 'Kindred');
$c = clone $a;                      // a different object
$c->titles[] = 'Solaris';

echo count($a->titles), ' ', count($c->titles), "\n";   // 1 2

Comparing objects

== is true when both objects are instances of the same class and their properties are equal (==); === is true only for the same instance. See Type Juggling and Comparisons for details.

<?php

declare(strict_types=1);

final class Isbn { public function __construct(public string $value) {} }

var_dump(new Isbn('1') == new Isbn('1'), new Isbn('1') === new Isbn('1'));   // bool(true) bool(false)

stdClass and conversions between arrays and objects

stdClass is PHP’s generic empty class: json_decode() without true, (object) casts and new stdClass() produce it, and it accepts any property. Casting an object to (array) gives its properties, with mangled keys for private ("\0Class\0prop") and protected ("\0*\0prop") ones; get_object_vars() returns only the properties visible in the current scope and is usually what you want.

<?php

declare(strict_types=1);

$row = (object) ['title' => 'Kindred', 'year' => 1979];
echo $row->title, ' ', get_class($row), "\n";    // Kindred stdClass

final class Book
{
    public function __construct(public string $title, private int $copies = 2) {}
    public function publicVars(): array { return get_object_vars($this); }
}

$book = new Book('Solaris');
var_dump(array_map(strlen(...), array_keys((array) $book)));   // the private key is "\0Book\0copies" (12 bytes)
print_r(get_object_vars($book));                               // ["title" => "Solaris"]: from outside
print_r($book->publicVars());                                  // ["title" => "Solaris", "copies" => 2]: from inside

For data transfer, prefer typed classes over stdClass; decode JSON into arrays (json_decode($json, true)) or map it to objects explicitly.

Anonymous classes

new class (…) { … } (7.0) creates an object of a class without a name, which can extend a class, implement interfaces and use traits. It is handy for one-off implementations in tests and for small adapters. Since 8.3 anonymous classes can be readonly.

<?php

declare(strict_types=1);

interface Clock { public function now(): DateTimeImmutable; }

function dueDate(Clock $clock, int $days): string
{
    return $clock->now()->modify("+{$days} days")->format('Y-m-d');
}

$fixed = new readonly class (new DateTimeImmutable('2026-10-05')) implements Clock {   // 8.3: readonly anonymous class
    public function __construct(private DateTimeImmutable $at) {}
    public function now(): DateTimeImmutable { return $this->at; }
};

echo dueDate($fixed, 21), "\n";                    // 2026-10-26
echo strstr($fixed::class, "\0", true), "\n";   // Clock@anonymous -- named after the first interface (8.0)

Introspection

get_class(), get_parent_class(), method_exists(), property_exists(), class_exists(), get_class_methods() and class_implements() answer basic questions about classes and objects at runtime; the Reflection API, covered in Reflection, answers all of them, including types and attributes.

<?php

declare(strict_types=1);

final class Catalogue implements Countable
{
    private array $books = [];
    public function count(): int { return count($this->books); }
}

$c = new Catalogue();
var_dump(get_class($c), method_exists($c, 'count'), property_exists(Catalogue::class, 'books'));
print_r(class_implements($c));    // ["Countable" => "Countable"]

Where older books and tutorials are outdated

  • PHP 4-style constructors (a method named like the class, Beak ch. 5) stopped working as constructors in PHP 8.0.

  • var for properties is a PHP 4 synonym for public; use explicit visibility.

  • Constructor boilerplate — assigning every parameter to a property — is replaced by constructor promotion (8.0).

  • Dynamic properties everywhere are deprecated since 8.2; books assign undeclared properties freely.

  • Untyped properties and constants — typed properties (7.4) and typed constants (8.3) are absent from books for PHP 7.3 and older.

  • (new Foo)->bar() parentheses are no longer required since 8.4.

  • __autoload() was removed in 8.0; see Namespaces and Autoloading.