PHP Static Properties and Methods

Static properties and methods belong to the class itself, not to any individual object created from that class. While a regular (instance) property has a separate copy for every object, a static property has exactly one copy that is shared across the entire class — every instance, and even code that never creates an instance at all, sees the same value. Static members are essential for counters, shared configuration, utility functions, and factory methods, and understanding them is a key step toward mastering PHP object-oriented programming.

Overview: How Static Members Work

When PHP compiles a class, the Zend Engine creates a single internal structure to represent that class, and static properties live inside that structure — not inside any object. Every time you create a new object with new, PHP allocates memory for the object’s instance properties, but it does not duplicate static properties. There is only one storage location for a static property for the lifetime of the request, shared by the class and all of its instances.

Static methods work the same way conceptually: they are called on the class, not on an object, so they do not receive an implicit $this variable. Because there is no object bound to the call, PHP has nothing to point $this at, which is why using $this inside a static method triggers a fatal error.

You access static members with the scope resolution operator :: rather than the object operator ->. Inside the class itself, you typically use self:: or the class name; from outside the class, you use ClassName::.

Syntax

<?php

class ClassName
{
    public static int $counter = 0;

    public static function increment(): void
    {
        self::$counter++;
    }
}

ClassName::increment();
ClassName::increment();

echo ClassName::$counter;

Output:

2
  • public static int $counter = 0; — declares a static property with a default value; the static keyword must appear alongside a visibility modifier (public, protected, or private).
  • public static function increment(): void — declares a static method; it can be called without ever instantiating the class.
  • self::$counter — accesses the static property from inside the class using self::.
  • ClassName::increment() and ClassName::$counter — access static members from outside the class using the class name and ::.

Examples

Example 1: A shared instance counter

<?php

class Counter
{
    public static int $count = 0;

    public function __construct()
    {
        self::$count++;
    }
}

new Counter();
new Counter();
new Counter();

echo "Total instances: " . Counter::$count;

Output:

Total instances: 3

Every time a Counter object is constructed, the constructor increments self::$count. Because the property is static, all three objects increment the same value rather than each having its own separate counter starting at zero.

Example 2: A stateless utility class

<?php

class MathHelper
{
    public static function square(int $n): int
    {
        return $n * $n;
    }

    public static function cube(int $n): int
    {
        return $n * $n * $n;
    }
}

echo MathHelper::square(5) . "\n";
echo MathHelper::cube(3) . "\n";

Output:

25
27

Neither square() nor cube() reads or writes any object state, so there is no reason to instantiate MathHelper at all. This is one of the most common legitimate uses of static methods: grouping related, self-contained utility functions under a namespace-like class name.

Example 3: Late static binding with static::

<?php

class Model
{
    public static function create(): static
    {
        return new static();
    }

    public function describe(): string
    {
        return "I am a " . static::class;
    }
}

class User extends Model
{
}

$model = Model::create();
$user = User::create();

echo $model->describe() . "\n";
echo $user->describe() . "\n";

Output:

I am a Model
I am a User

Both create() methods are inherited from Model, but new static() and static::class resolve to the class that was actually called at runtime, not the class where the method was defined. This behavior is called late static binding, and it is what makes static:: different from self::.

Under the Hood: self:: vs static::

self:: always refers to the class in which the code is physically written — it is resolved at compile time, based on the class definition. static::, introduced for late static binding, is resolved at runtime based on which class was originally invoked in the call chain. If Model::create() had used new self() instead of new static(), then User::create() would incorrectly return a Model instance instead of a User instance, because self would freeze the reference to Model at the point the method was defined.

This distinction matters most in factory methods, abstract base classes, and any pattern where a parent class needs to create or reference “whatever subclass is actually being used,” rather than itself.

Common Mistakes

Mistake 1: Using $this inside a static method

Static methods are not called on an object, so there is no $this available. This raises a fatal error at runtime.

class Widget
{
    public static int $total = 0;

    public static function increment()
    {
        $this->total++;
    }
}

Widget::increment();

The fix is to use self:: (or static::) to access the static property instead of $this:

<?php

class Widget
{
    public static int $total = 0;

    public static function increment(): void
    {
        self::$total++;
    }
}

Widget::increment();

echo Widget::$total;

Output:

1

Mistake 2: Expecting a static property to be per-instance

Because static properties are shared by the whole class, using one to hold data you actually want scoped to a single object leads to unexpected shared state:

<?php

class ShoppingCart
{
    public static array $items = [];

    public function addItem(string $item): void
    {
        self::$items[] = $item;
    }
}

$cart1 = new ShoppingCart();
$cart1->addItem('Book');

$cart2 = new ShoppingCart();
$cart2->addItem('Pen');

echo count(ShoppingCart::$items);

Output:

2

A developer might expect each cart to hold only its own item, but since $items is static, both carts write into the same shared array, so the count is 2 instead of the expected 1. The fix is to use a regular (instance) property so each object gets its own copy:

<?php

class ShoppingCart
{
    private array $items = [];

    public function addItem(string $item): void
    {
        $this->items[] = $item;
    }

    public function getItems(): array
    {
        return $this->items;
    }
}

$cart1 = new ShoppingCart();
$cart1->addItem('Book');

$cart2 = new ShoppingCart();
$cart2->addItem('Pen');

echo count($cart1->getItems());

Output:

1

Best Practices

  • Use static members for data or behavior that genuinely belongs to the class as a whole (counters, caches, configuration, utility functions) — not for per-object state.
  • Prefer static:: over self:: inside methods that subclasses might call via inheritance, so late static binding resolves to the correct subclass.
  • Keep static utility classes stateless where possible; stateful static properties make code harder to test because state persists across calls and tests.
  • Avoid overusing static methods as a substitute for dependency injection — heavy reliance on static calls makes classes difficult to mock and unit test.
  • Always declare an explicit visibility modifier (public, protected, or private) alongside static rather than relying on defaults.
  • Use type declarations on static properties and return types on static methods just as you would for instance members — they are just as important for catching bugs early.

Practice Exercises

  1. Write a class called IdGenerator with a static property $nextId starting at 1 and a static method next() that returns the current value of $nextId and then increments it. Call next() three times and print each returned value.
  2. Write a base class Shape with a static factory method make(): static that returns new static(), and a method whatAmI() that returns static::class. Create two subclasses, Circle and Square, and show that calling make() on each returns the correct subclass instance.
  3. Refactor a class that mistakenly stores per-user data (such as a $loginCount) in a static property so that it instead uses an instance property, and explain in a comment why the original version was a bug.

Summary

  • Static properties and methods belong to the class itself, not to any single object, and are shared by all instances.
  • Access static members with :: — using ClassName::$property or ClassName::method() from outside, and self:: from inside the class.
  • Static methods have no $this, because they are not bound to any object instance.
  • static:: enables late static binding, resolving to the class actually invoked at runtime rather than the class where the code was written; self:: always resolves to the defining class.
  • Use static members for shared, class-level concerns like counters and utility functions, and instance properties for anything that should differ per object.