Lesson 05 / 12
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.
Published Updated
In this lesson
- 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
Contents
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 // 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 // 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 // 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 // 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 // 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 // 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 // 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 // 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 // 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 // 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 // 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 // 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 // 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 // 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 // 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 // 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
20to18, the consumer was affected implicitly: exit code 0, output moved from120to118.
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.
To keep your progress and take notes, Log in
My notes
Log in to take notes.