---
title: 'Static Members and Constants'
source: 'https://academia.sh/en/courses/php-object-model/static-members-and-constants'
course: 'Object-Oriented PHP and Standards'
language: en
updated: '2026-10-21T05:03:35+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Hides the constructor and creates the object through a static factory method'
    - 'Measures that adding a parameter to the static factory does not stop the consumer'
    - 'Compares how hiding the constructor itself and changing the value of a class constant affect the consumer'
---

# Static Members and Constants

Properties, methods, and constants bound to the class, hiding the constructor behind a static factory method, and which changes stop the consumer.

Traits shared methods; this lesson is about members bound not to an object but to the class itself. The question is this: once object creation moves into a static factory method, why does adding a parameter to the constructor not stop the consumer, and why would it have been different if the constructor had stayed open?

## Static Members

A static member belongs to the class and is reached with `ClassName::` without creating an object. From inside the class, the same class is reached with `self::`. A static property has a single copy per class, and all objects share it. A class constant is declared with `const`, its value never changes, and it is read with `self::RATE` or `Tax::RATE`.

```php
<?php
// Tax.php — a constant, property, and method bound to the class
class Tax
{
    public const RATE = 20;
    public static int $computed = 0;

    public static function add(int $amount): int
    {
        self::$computed++;
        return $amount + intdiv($amount * self::RATE, 100);
    }
}
```

```php
<?php
// tax_usage.php — static members are reached without an object
require 'Tax.php';

echo Tax::add(100), "\n";
echo Tax::add(200), "\n";
echo Tax::RATE, ' ', Tax::$computed, "\n";
```

```
120
240
20 2
```

**OM17.** No object was created for `Tax`, and `add()` ran twice; `$computed` counted both calls in a single copy. This sharing was measured in M11/K03 with the question of who sees what, and it is not measured again here. The use of `static::`, which asks for the class to be resolved again in a subclass (late static binding), is the subject of M08/K02 and is not measured here.

## Static Factory Method

A static factory method is a static method that creates an object. When the constructor is made `private`, the object can come only from this method. The class thus limits for itself which values an object can be created with.

```php
<?php
// Money.php — the constructor is hidden, a static factory creates the object
class Money
{
    private function __construct(private int $cents) {}

    public static function dollars(int $dollars): self
    {
        return new self($dollars * 100);
    }

    public function cents(): int
    {
        return $this->cents;
    }
}
```

```php
<?php
// money_usage.php — the object is created only through the factory
require 'Money.php';

echo Money::dollars(5)->cents(), "\n";

try {
    $money = new Money(500);
} catch (Error $e) {
    echo get_class($e), "\n";
}
```

```
500
Error
```

**OM18.** The factory turned `5` dollars into `500` cents, while a direct `new Money(500)` raised an `Error` because the constructor is closed. The consumer's only path became `Money::dollars()`. The question of this lesson is why that single path can be changed.

## Object Context and Static Context

An instance method works on an object, and `$this` is that object. A static method has no object, so `$this` cannot be used; only static members are reached, with `self::`. The same class can carry both contexts: an instance property is separate in each object, while a static property is a single copy.

```php
<?php
// Counter.php — an instance property and a static property together
class Counter
{
    private static int $total = 0;
    private int $local = 0;

    public function increment(): string
    {
        self::$total++;
        $this->local++;
        return $this->local . '/' . self::$total;
    }

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

    public static function broken(): int
    {
        return $this->local;
    }
}
```

```php
<?php
// counter_usage.php — the local counters of two objects are separate, the total is shared
require 'Counter.php';

$first = new Counter();
$second = new Counter();

echo $first->increment(), ' ', $first->increment(), ' ', $second->increment(), "\n";
echo Counter::total(), "\n";

try {
    Counter::broken();
} catch (Error $e) {
    echo get_class($e), "\n";
}
```

```
1/1 2/2 1/3
3
Error
```

`$second` saw `1` on its own counter but `3` in the total: both calls of the first object had written to the same static property. Looking for `$this` in a static method raised an `Error`. A static call that resolves differently in subclasses uses `static::`; this behavior, called late static binding, was measured in M08/K02 and is not repeated here.

## Visibility of a Class Constant

A class constant carries visibility just like a property and a method. A `public const` is read from anywhere, a `private const` only from inside the class itself. A constant definition may rest on another constant, but not on a non-constant expression such as a function call.

```php
<?php
// Settings.php — constants with different visibility
class Settings
{
    public const NAME = 'panel';
    private const ORDER = 5;
    public const FACTOR = self::ORDER * 2;

    public static function order(): int
    {
        return self::ORDER;
    }
}
```

```php
<?php
// settings_usage.php — a private constant cannot be reached from outside the class
require 'Settings.php';

echo Settings::NAME, ' ', Settings::FACTOR, ' ', Settings::order(), "\n";

try {
    echo Settings::ORDER;
} catch (Error $e) {
    echo get_class($e), "\n";
}
```

```
panel 10 5
Error
```

`FACTOR` was derived from `ORDER` inside the class and could be read from outside; `ORDER` left the class only through the `order()` method. This shows that the property visibility from the Classes and Objects lesson applies to a constant unchanged: making a constant `private` limits which consumers a change in its value can reach.

## The Measurement Core

The measurement in this lesson also uses the `reach.php` file; the core was explained line by line in the first lesson.

```php
<?php
// reach.php — helpers that measure where a change reaches

function write_files(string $dir, array $files): void
{
    foreach ($files as $name => $content) {
        if ($content === null) {
            continue; // null: the file was deleted
        }
        $path = "$dir/$name";
        if (!is_dir(dirname($path))) {
            mkdir(dirname($path), 0777, true);
        }
        file_put_contents($path, $content);
    }
}

function read_files(string $dir): array
{
    $files = [];
    $walker = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)
    );
    foreach ($walker as $file) {
        $name = substr($file->getPathname(), strlen($dir) + 1);
        $files[$name] = file_get_contents($file->getPathname());
    }
    return $files;
}

function run(string $dir): array
{
    $command = 'cd ' . escapeshellarg($dir) . ' && php -d display_errors=0 main.php 2>/dev/null';
    exec($command, $lines, $code);
    return [$code, implode("\n", $lines)];
}

function measure(string $name, array $base, array $change, array $seams = []): string
{
    if (array_key_exists('main.php', $change)) {
        throw new LogicException("$name: the consumer is not edited");
    }
    write_files("measure/$name/before", $base);
    write_files("measure/$name/after", array_merge($base, $change));
    [$codeBefore, $outputBefore] = run("measure/$name/before");
    [$codeAfter, $outputAfter] = run("measure/$name/after");
    if ($codeBefore !== 0) {
        throw new LogicException("$name: the baseline did not run");
    }
    $reach = match (true) {
        $codeAfter !== 0 => 'breaking',
        $outputBefore !== $outputAfter => 'implicit',
        array_intersect(array_keys($change), $seams) !== [] => 'at-seam',
        default => 'in-place',
    };
    printf(
        "%-18s files=%d  exit=%-3d output %-7s -> %s\n",
        $name,
        count($change),
        $codeAfter,
        $outputBefore === $outputAfter ? 'same' : 'changed',
        $reach
    );
    return $reach;
}
```

## Adding a Parameter to the Factory

The measured change is adding a second parameter named `currency` to the constructor of the `Money` class. The factory supplies this parameter itself: `dollars()` passes `'USD'` to the constructor. The consumer `main.php` prints the value of `Money::dollars(5)->cents()`.

```php
<?php
// factory.php — a parameter is added to the constructor, the factory gives the consumer the same value unchanged
require_once 'reach.php';

measure('static-factory', [
    'money.php' => <<<'PHP'
        <?php
        class Money
        {
            private function __construct(private int $cents) {}
            public static function dollars(int $dollars): self { return new self($dollars * 100); }
            public function cents(): int { return $this->cents; }
        }
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'money.php';
        echo Money::dollars(5)->cents(), "\n";
        PHP,
], [
    'money.php' => <<<'PHP'
        <?php
        class Money
        {
            private function __construct(private int $cents, private string $currency) {}
            public static function dollars(int $dollars): self { return new self($dollars * 100, 'USD'); }
            public function cents(): int { return $this->cents; }
        }
        PHP,
]);

foreach (['before', 'after'] as $stage) {
    [$code, $output] = run("measure/static-factory/$stage");
    echo $stage, ' exit=', $code, ' output=', $output, "\n";
}
```

```
static-factory     files=1  exit=0   output same    -> in-place
before exit=0 output=500
after exit=0 output=500
```

**OM19.** The signature of the constructor changed, but because what the consumer sees is `Money::dollars(5)->cents()`, the output stayed `500`. Only the factory called the constructor, and the party that wrote the factory supplied the new parameter inside it. The lesson's first reach is **in place**.

## The Bounding Measurement

Is the change staying in place because of the factory, or because of the change itself? We can separate the two by leaving the constructor open and letting the consumer say `new Money(500)`: when the same parameter is added, the consumer depends directly on the constructor.

```php
<?php
// public_constructor.php — the constructor is open, the consumer calls it directly with new
require_once 'reach.php';

measure('public-constructor', [
    'money.php' => <<<'PHP'
        <?php
        class Money
        {
            public function __construct(private int $cents) {}
            public function cents(): int { return $this->cents; }
        }
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'money.php';
        echo (new Money(500))->cents(), "\n";
        PHP,
], [
    'money.php' => <<<'PHP'
        <?php
        class Money
        {
            public function __construct(private int $cents, private string $currency) {}
            public function cents(): int { return $this->cents; }
        }
        PHP,
]);

foreach (['before', 'after'] as $stage) {
    [$code, $output] = run("measure/public-constructor/$stage");
    echo $stage, ' exit=', $code, ' output=', $output, "\n";
}
```

```
public-constructor files=1  exit=255 output changed -> breaking
before exit=0 output=500
after exit=255 output=
```

**OM20.** The same added parameter, the same class; but once the consumer called the constructor itself, the second parameter became required and the exit code was 255. So what kept the change in place was not the parameter itself but the fact that the consumer could not touch the constructor at all. The factory gathers the dependency on the constructor into a single point.

A class constant belongs to another group. If the value of a class constant changes, nothing stops the consumer, because the type and the signature stay the same.

```php
<?php
// constant.php — the value of a class constant changes, the signature stays the same
require_once 'reach.php';

measure('class-constant', [
    'tax.php' => <<<'PHP'
        <?php
        class Tax
        {
            public const RATE = 20;
            public static function add(int $amount): int { return $amount + intdiv($amount * self::RATE, 100); }
        }
        PHP,
    'main.php' => <<<'PHP'
        <?php
        require 'tax.php';
        echo Tax::add(100), "\n";
        PHP,
], [
    'tax.php' => <<<'PHP'
        <?php
        class Tax
        {
            public const RATE = 18;
            public static function add(int $amount): int { return $amount + intdiv($amount * self::RATE, 100); }
        }
        PHP,
]);

foreach (['before', 'after'] as $stage) {
    [$code, $output] = run("measure/class-constant/$stage");
    echo $stage, ' exit=', $code, ' output=', $output, "\n";
}
```

```
class-constant     files=1  exit=0   output changed -> implicit
before exit=0 output=120
after exit=0 output=118
```

**OM21.** `RATE` dropped from `20` to `18`, the consumer received no error, but the value it received moved from `120` to `118`: exit code 0, output changed. Because the value of a constant is not treated as a contract, nothing stopped. This reach is **implicit**.

## A Static Property in a Subclass

A subclass inherits the static property of its parent but does not get a new copy: the two share the same storage. If the subclass redeclares the property in its own body, a separate copy is born. The difference between the two matters when structures such as a counter or a registry are extended through inheritance.

```php
<?php
// Sharing.php — the static property of the parent is shared with the subclass
class Base
{
    public static int $count = 0;
}

class Shared extends Base
{
}

class Separate extends Base
{
    public static int $count = 0;
}
```

```php
<?php
// sharing_usage.php — Shared shares with Base, Separate does not
require 'Sharing.php';

Shared::$count = 5;
echo Base::$count, "\n";

Separate::$count = 9;
echo Base::$count, ' ', Separate::$count, "\n";
```

```
5
5 9
```

The expression `Shared::$count` actually wrote to the property of `Base`; that is why `Base::$count` also gave `5`. `Separate` redeclared the property, so it kept its own value and did not affect `Base`. Code that wants to give subclasses separate counters has to redeclare the property in every subclass; if it is forgotten, the counters merge silently.

## Static Members and Difficulty in Testing

Because a static member is shared and reachable from anywhere by the class name, what state a piece of code runs in depends on the earlier calls. Test scenarios should be independent of one another; static state breaks this independence. The two scenarios below are written separately, but the second sees the data the first left behind.

```php
<?php
// Cache.php — shared state reached by the class name
class Cache
{
    private static array $data = [];

    public static function put(string $key, int $value): void
    {
        self::$data[$key] = $value;
    }

    public static function size(): int
    {
        return count(self::$data);
    }
}
```

```php
<?php
// scenario.php — the second scenario sees the data the first one left behind
require 'Cache.php';

function firstScenario(): int
{
    Cache::put('a', 1);
    return Cache::size();
}

function secondScenario(): int
{
    return Cache::size();
}

echo firstScenario(), ' ', secondScenario(), "\n";
```

```
1 1
```

`secondScenario()` put nothing in and still saw `1`. Run on its own it would have seen `0`; run in sequence, the order of execution determines the result. Keeping the same data as a property of an object and passing the object in from outside breaks this dependency: each scenario creates its own object and does not see what another left behind. Passing a dependency in from outside will be covered separately at the end of the course; this lesson only showed why static state calls for that solution.

## Summary

- A static member is bound to the class; it is reached with `self::`, a static property has a single copy per class, and a class constant never changes.
- Hiding the constructor and creating the object through a static factory method gathers the way objects are created into a single point.
- When a parameter was added to the constructor, the consumer that creates through the factory stayed in place, with output `500`; the lesson's reach is in place.
- The same addition was breaking when the constructor was open and the consumer said `new Money(500)`, with exit code 255.
- When the value of a class constant dropped from `20` to `18`, the consumer was affected implicitly: exit code 0, output moved from `120` to `118`.

## Next Step

A constant's value shifted the consumer silently; magic methods, on the other hand, turn reading itself into a method. The Magic Methods lesson asks what the consumer sees when the content of the `__get` method, through which a property is read, changes.
