PHP Static Properties
What Is a Static Property?
In simple terms: A normal property belongs to each object — every product has its own price. A static property belongs to the class and is shared by all objects of that class. There is exactly one copy, no matter how many objects exist (even zero). It is like a counter on a shop's door that counts every visitor, rather than a badge each visitor carries.
Technically: A property declared with the static keyword is stored once per class. It is accessed with ClassName::$property from outside, or self::$property / static::$property inside the class — note the $ after ::. Static properties keep their values for the duration of the request (or process), across all objects and method calls.
| Instance Property | Static Property | |
|---|---|---|
| Declared as | private int $count | private static int $count |
| Storage | One per object | One per class |
| Access | $this->count, $obj->count | self::$count, ClassName::$count |
| Exists without objects | No | Yes |
Declaring and Using Static Properties
<?php
final class Visitor
{
private static int $count = 0;
public function __construct(public readonly string $name)
{
self::$count++;
}
public static function count(): int
{
return self::$count;
}
}
echo Visitor::count() . "\n";
$a = new Visitor('Asha');
$b = new Visitor('Bala');
$c = new Visitor('Charu');
echo Visitor::count() . "\n";
Output
0
3
How It Works
private static int $count = 0;creates a single counter for the whole class.- Every constructor call increments the same counter through
self::$count. Visitor::count()can be called before any object exists; the property is available as soon as the class is loaded.- The property is
private, so only the class can change it; a static method exposes the value.
Syntax Reminder
self::$count // static property: $ after ::
self::MAX_ITEMS // constant: no $
self::create() // static method
$this->name // instance property: no $ after ->
Static Properties and Inheritance
A static property declared in a parent class is shared by the parent and all child classes — unless a child class redeclares it:
<?php
class Model
{
protected static int $queries = 0;
public static function recordQuery(): void
{
static::$queries++;
}
public static function queries(): int
{
return static::$queries;
}
}
class User extends Model {}
class Product extends Model {}
class Order extends Model
{
protected static int $queries = 0; // redeclared: Order gets its own copy
}
User::recordQuery();
User::recordQuery();
Product::recordQuery();
Order::recordQuery();
echo "User: " . User::queries() . "\n";
echo "Product: " . Product::queries() . "\n";
echo "Model: " . Model::queries() . "\n";
echo "Order: " . Order::queries() . "\n";
Output
User: 3
Product: 3
Model: 3
Order: 1
What This Shows
UserandProductdo not redeclare$queries, so they share the property declared inModel: all three calls changed the same counter.Orderredeclares the property, so it has a separate counter.static::$queriesrefers to the property of the class the method was called on (late static binding, see Late Static Binding); withself::$queries,Order::recordQuery()would have changedModel's counter instead.
Shared static state in a hierarchy is a common source of surprises. If each subclass needs its own value, redeclare the property or use an array keyed by static::class.
Practical Example: Caching Expensive Lookups
Static properties can cache results so that an expensive operation (a database query or API call) runs only once per request:
<?php
final class CurrencyRates
{
private static array $cache = [];
private static int $lookups = 0;
public static function rate(string $currency): float
{
$currency = strtoupper($currency);
if (!isset(self::$cache[$currency])) {
self::$lookups++;
self::$cache[$currency] = self::fetchRate($currency);
}
return self::$cache[$currency];
}
public static function lookups(): int
{
return self::$lookups;
}
private static function fetchRate(string $currency): float
{
// Simulates a slow API call
return match ($currency) {
'USD' => 83.2,
'EUR' => 90.5,
default => throw new InvalidArgumentException("Unknown currency: $currency"),
};
}
}
foreach (['USD', 'usd', 'EUR', 'USD'] as $currency) {
echo "1 $currency = ₹" . CurrencyRates::rate($currency) . "\n";
}
echo "API lookups: " . CurrencyRates::lookups() . "\n";
Output
1 USD = ₹83.2
1 usd = ₹83.2
1 EUR = ₹90.5
1 USD = ₹83.2
API lookups: 2
Step-by-Step Explanation
- The first request for a currency calls
fetchRate()and stores the result in the static$cache. - Later requests for the same currency (including
usd, normalized toUSD) are answered from the cache. - Four calls cause only two lookups.
This is called memoization. In real applications, a cache object (injected into the classes that need it, or a PSR-16 cache) is usually preferable, because it can be configured, shared across requests (Redis), and replaced in tests.
The Risks of Static State
Static properties behave like global variables scoped to a class. That brings the same risks:
| Risk | Example |
|---|---|
| Hidden coupling | Any code anywhere can change the shared value (if it is public) |
| Test interference | One test fills a static cache; the next test sees stale data |
| Long-running processes | In queue workers or application servers (Swoole, RoadRunner, FrankenPHP worker mode), static values survive between requests and can leak one user's data into another user's request |
| Inheritance surprises | Child classes unintentionally share the parent's value |
Guidelines
- Keep static properties
privateand change them only through methods of the class. - Use them for data that is truly class-wide and safe to share: counters for debugging, caches of immutable data, registries filled at startup.
- Provide a reset method for tests if necessary.
- Never store per-user or per-request data in static properties when the code may run in a long-running process.
Rules and Details
| Rule | Detail |
|---|---|
| Typed static properties | Supported, e.g. private static array $cache = []; |
| Default values | Constant expressions only, like other properties |
readonly | Not allowed for static properties |
| Access through an object | $obj::$prop works; $obj->prop does not access the static property (it emits a notice and treats it as an undefined instance property) |
| Visibility | public, protected, private apply as usual |
Common Mistakes
Forgetting the $ After ::
Effect: self::count refers to a constant named count, causing an "Undefined constant" error.
Fix: self::$count.
Using $this for Static Properties
Effect: $this->count refers to an instance property, not the static one.
Fix: Use self::$count or static::$count.
Using Static Properties as Global Variables
Effect: State changes from anywhere, hard-to-trace bugs.
Fix: Pass data explicitly or inject shared services.
Assuming Child Classes Have Their Own Copy
Effect: All subclasses share and overwrite one value.
Fix: Redeclare the property in each subclass, or key values by static::class.
Best Practices
- Use static properties sparingly, for genuinely class-wide data.
- Keep them private and encapsulated behind static methods.
- Prefer injected objects (caches, registries, configuration) for shared state in applications.
- Be careful with static state in long-running PHP processes.
- Remember:
self::$propfor properties,self::CONSTfor constants.
Official reference: https://www.php.net/manual/en/language.oop5.static.php