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?allowsnullin 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,: intafter 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 thannull. - The constructor executes with
$thisbound 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 resolvesmethodby walking the class’s method table (and its parent classes if not found locally), binds$thisto$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$thisavailable inside a static method. - For a
readonlyproperty, 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 anErrorinstead 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
publicdefault. - 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
readonlywhenever a property represents an identity or value that should never change after construction. - Keep properties
privateorprotectedby 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 reservestatic::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
Rectangleclass with private typed properties$widthand$height, a constructor that sets them, and a methodarea(): floatthat returns their product. Instantiate one with width4.5and height2.0and echo the area. - Create a
Counterclass with a private staticint $totalproperty and a public static methodincrement(): intthat increases the total by one and returns the new value. Callincrement()three times and print the result each time. - Build an immutable
Temperatureclass with areadonly float $celsiusproperty set through the constructor, plus a methodtoFahrenheit(): float. Work out what happens if you try to reassign$celsiusafter 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
readonlyproperties can be written only once. staticproperties and methods belong to the class itself, not to individual instances, and are accessed with::.$thisrefers to the calling object inside instance methods; there is no$thisinside a static method.- Constructor property promotion is a concise shorthand for declaring and assigning properties in one place.
