PHP Properties and Methods

Properties are the variables that belong to a class and its objects, and methods are the functions that belong to a class and operate on that data. Together they let you bundle related state and behavior into a single reusable unit instead of juggling loose variables and disconnected functions. Understanding exactly how properties are declared, typed, scoped, and accessed — and how methods reach that data through $this — is the foundation for everything else in PHP object-oriented programming, from constructors to inheritance and interfaces.

Overview: How Properties and Methods Work

A class in PHP is a blueprint that defines two kinds of members: properties (variables) and methods (functions). When you create an object with new, PHP allocates a fresh property table for that object in memory, one slot per non-static property, and copies in any default values you declared. Methods, by contrast, are not duplicated per object — the Zend Engine compiles each method once into the class’s method table, and every object of that class shares the exact same compiled code. When you call $object->method(), PHP looks up the method in the class’s method table and executes it with an implicit variable, $this, bound to the specific object you called it on. That is how one method body can read and modify different data for different objects.

Properties and methods can carry a visibility modifier: public, protected, or private. The engine checks visibility on every access from outside the declaring class. public members are reachable from anywhere, protected members are reachable from the class itself and its subclasses, and private members are reachable only from the exact class that declared them, not even from subclasses. If you omit a modifier on a property, PHP treats it as public, but writing it explicitly is considered good practice for both properties and methods.

Since PHP 7.4, properties can carry a type declaration such as string, int, float, bool, array, a class name, or a union like int|string. A typed property with no default value is left uninitialized rather than null, and reading it before it has been assigned throws an Error. This is a deliberate safety net: it stops the classic bug where a missed assignment silently produces a stray null that spreads through your program. Since PHP 8.1, a typed property can also be marked readonly, meaning it may be written exactly once — almost always inside the constructor — and any later write attempt, even from inside the declaring class, throws an Error.

Properties and methods can also be declared static, which detaches them from individual objects and attaches them to the class itself. A static property has exactly one storage location shared by every instance (and it exists even if no instance has ever been created), and a static method has no $this because it is not called on a specific object. You reach both with the scope resolution operator ::, either through the class name from outside, or through self:: from inside the class.

Syntax

<?php

class Example {
    public string $name = "unnamed";
    protected int $count = 0;
    private ?array $items = null;
    public readonly string $id;
    public static int $instances = 0;

    public function __construct(string $id) {
        $this->id = $id;
        self::$instances++;
    }

    public function methodName(int $arg): string {
        return "{$this->name}-{$arg}";
    }

    public static function staticMethod(): int {
        return self::$instances;
    }
}
  • public / protected / private — the visibility modifier controlling which code can access the member.
  • string, int, ?array, etc. — the property’s type declaration; a leading ? allows null in addition to the given type.
  • $name — the property name, always referenced with a leading $.
  • = "unnamed" — an optional default value, allowed only when it can be determined without running code.
  • readonly — the property may be assigned once, typically in the constructor, and never modified again.
  • static — the member belongs to the class itself, shared by every instance, and is accessed with :: instead of ->.
  • $this — inside a non-static method, refers to the object the method was called on.
  • self:: — inside the class, refers to the class itself, used for static members and constants.
  • : string, : int after the parameter list — the method’s return type declaration.

Examples

Example 1: Basic properties and a method

<?php

class Car {
    public string $make;
    public string $model;
    public int $year;

    public function __construct(string $make, string $model, int $year) {
        $this->make = $make;
        $this->model = $model;
        $this->year = $year;
    }

    public function describe(): string {
        return "{$this->year} {$this->make} {$this->model}";
    }
}

$car = new Car("Toyota", "Corolla", 2023);
echo $car->describe();
echo "\n";
echo $car->make;

Output:

2023 Toyota Corolla
Toyota

The constructor assigns each parameter to a property on $this, and describe() reads those same properties back through string interpolation. Because $make, $model, and $year are typed public properties, they can also be read directly from outside the class, as shown by the last line.

Example 2: Constructor promotion and readonly properties

<?php

class BankAccount {
    private float $balance = 0.0;

    public function __construct(
        public readonly string $owner,
        public readonly string $accountNumber,
    ) {}

    public function deposit(float $amount): void {
        if ($amount <= 0) {
            throw new InvalidArgumentException("Deposit must be positive");
        }
        $this->balance += $amount;
    }

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

$account = new BankAccount("Amara Okafor", "ACC-10293");
$account->deposit(150.75);
$account->deposit(49.25);

echo $account->owner . "\n";
echo $account->getBalance();

Output:

Amara Okafor
200

The constructor parameters $owner and $accountNumber use constructor property promotion: writing the visibility and readonly keyword directly on a constructor parameter declares the property and assigns it in one step, with no body needed. $balance stays private and can only change through the deposit() method, which is the encapsulation pattern you want for anything that needs validation before it changes.

Example 3: Static properties and methods

<?php

class Widget {
    private static int $count = 0;
    public string $name;

    public function __construct(string $name) {
        $this->name = $name;
        self::$count++;
    }

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

$a = new Widget("A");
$b = new Widget("B");
$c = new Widget("C");

echo Widget::getCount();

Output:

3

$count is declared static, so there is exactly one copy of it shared by every Widget instance, not one copy per object. Each constructor call increments the same shared value through self::$count, and getCount() is called on the class itself with :: rather than on any single instance.

How It Works Step by Step

Walking through what PHP actually does when you use properties and methods clarifies why the rules above exist:

  • When new ClassName(...) runs, the engine allocates a property table sized to the class’s non-static properties. Properties with defaults are pre-filled; typed properties without defaults are marked uninitialized rather than null.
  • The constructor executes with $this bound to the new object. Promoted constructor parameters are handled specially: the engine both declares the property and assigns it from the argument, before the constructor body even runs.
  • When you call $object->method(...), PHP resolves method by walking the class’s method table (and its parent classes if not found locally), binds $this to $object, and executes the compiled bytecode.
  • A call like ClassName::staticMethod() skips object resolution entirely; it goes straight to the class’s static storage, which is why there is no $this available inside a static method.
  • For a readonly property, the engine tracks whether it has already been initialized from within its declaring scope. The first write succeeds; every subsequent write attempt, from anywhere, throws an Error instead of silently overwriting the value.

Common Mistakes

Mistake 1: Forgetting $this and reassigning the parameter instead

<?php

class Product {
    public string $name;
    public float $price;

    public function __construct(string $name, float $price) {
        $name = $name;
        $price = $price;
    }
}

This parses fine, but it never touches the object at all — $name = $name; just reassigns the local constructor parameter to itself. The $name and $price properties on the object stay uninitialized, so reading $product->name later throws an Error. The fix is to always qualify property access with $this->:

<?php

class Product {
    public string $name;
    public float $price;

    public function __construct(string $name, float $price) {
        $this->name = $name;
        $this->price = $price;
    }
}

$p = new Product("Keyboard", 49.99);
echo $p->name;

Output:

Keyboard

Mistake 2: Trying to modify a readonly property after construction

<?php

class Point {
    public function __construct(
        public readonly float $x,
        public readonly float $y,
    ) {}

    public function moveTo(float $x, float $y): void {
        $this->x = $x;
        $this->y = $y;
    }
}

This compiles, but calling moveTo() on any instance throws an Error: Cannot modify readonly property Point::$x, because readonly properties reject any write after their first initialization — even one coming from a method of the same class. The idiomatic fix is to return a new instance instead of mutating the existing one:

<?php

class Point {
    public function __construct(
        public readonly float $x,
        public readonly float $y,
    ) {}

    public function withCoordinates(float $x, float $y): self {
        return new self($x, $y);
    }
}

$p1 = new Point(1.0, 2.0);
$p2 = $p1->withCoordinates(5.0, 6.0);

echo $p1->x . "," . $p1->y . "\n";
echo $p2->x . "," . $p2->y;

Output:

1,2
5,6

Mistake 3: Exposing no way to read a private property

<?php

class User {
    private string $email;

    public function __construct(string $email) {
        $this->email = $email;
    }
}

With only this definition, any code outside the class that tries $user->email triggers a fatal Error: Cannot access private property User::$email. Making a property private is correct for encapsulation, but it also means you must deliberately provide a public method if outside code legitimately needs the value:

<?php

class User {
    private string $email;

    public function __construct(string $email) {
        $this->email = $email;
    }

    public function getEmail(): string {
        return $this->email;
    }
}

$user = new User("test@example.com");
echo $user->getEmail();

Output:

test@example.com

Best Practices

  • Declare visibility explicitly on every property and method instead of relying on the implicit public default.
  • Add a type declaration to every property so PHP catches missing or incorrect assignments as early as possible.
  • Use constructor property promotion for simple data-holding classes to remove repetitive boilerplate.
  • Reach for readonly whenever a property represents an identity or value that should never change after construction.
  • Keep properties private or protected by default and expose behavior through methods, only widening visibility when outside code genuinely needs direct access.
  • Use self:: to refer to the declaring class’s own static members, and reserve static:: for cases where you specifically want late static binding in a class hierarchy.
  • Keep each method focused on one responsibility; a method that both validates and persists and formats output is a sign the class is doing too much.

Practice Exercises

  • Write a Rectangle class with private typed properties $width and $height, a constructor that sets them, and a method area(): float that returns their product. Instantiate one with width 4.5 and height 2.0 and echo the area.
  • Create a Counter class with a private static int $total property and a public static method increment(): int that increases the total by one and returns the new value. Call increment() three times and print the result each time.
  • Build an immutable Temperature class with a readonly float $celsius property set through the constructor, plus a method toFahrenheit(): float. Work out what happens if you try to reassign $celsius after the object is created, and explain why.

Summary

  • Properties store an object’s data; methods define its behavior.
  • Non-static properties live in a per-object property table; methods are compiled once and shared through the class’s method table.
  • Visibility (public, protected, private) controls access from outside code and from subclasses.
  • Typed properties must be initialized before they are read, and readonly properties can be written only once.
  • static properties and methods belong to the class itself, not to individual instances, and are accessed with ::.
  • $this refers to the calling object inside instance methods; there is no $this inside a static method.
  • Constructor property promotion is a concise shorthand for declaring and assigning properties in one place.