1 — Static Properties & Methods
Everything covered so far has been instance state — each object gets its own copy. Static properties and methods belong to the class itself, not to any particular instance. They exist once, shared across all instances and accessible without creating an object at all.
Access them with the scope resolution operator
:: — called "double colon" or "Paamayim Nekudotayim"
(you'll see that in PHP error messages). From inside the class, use
self:: to refer to the current class statically.
class Counter { // One shared value across ALL Counter instances private static int $count = 0; public function __construct() { self::$count++; // increment shared counter on each new instance } public static function getCount(): int { return self::$count; } public static function reset(): void { self::$count = 0; } } new Counter(); new Counter(); new Counter(); echo Counter::getCount(); // 3 — no object needed to call this Counter::reset(); echo Counter::getCount(); // 0
Static is class-level — the consequences
- Static methods have no
$this— there is no object context - Static properties are shared in memory — every part of the program sees the same value
- They persist for the life of the request (PHP's process model), which can cause state leakage between tests
- Static makes code harder to mock/test — use sparingly and with purpose
Legitimate uses for static methods
Not all static is bad. The right uses are stateless utility methods and named constructors (factory methods) that improve readability:
class Color { private function __construct( // private! Forces use of named constructors private int $r, private int $g, private int $b ) {} // Named constructors — each tells you exactly what it creates public static function fromRgb(int $r, int $g, int $b): static { return new static($r, $g, $b); } public static function fromHex(string $hex): static { $hex = ltrim($hex, '#'); return new static( hexdec(substr($hex, 0, 2)), hexdec(substr($hex, 2, 2)), hexdec(substr($hex, 4, 2)) ); } public static function red(): static { return new static(255, 0, 0); } public static function green(): static { return new static(0, 255, 0); } public static function blue(): static { return new static(0, 0, 255); } public function toHex(): string { return sprintf('#%02x%02x%02x', $this->r, $this->g, $this->b); } } // Much more readable than new Color(255, 0, 0): $crimson = Color::fromHex('#DC143C'); $primary = Color::blue(); $custom = Color::fromRgb(100, 200, 50);
Notice new static instead of new self
in the factory methods. This is the setup for the next topic — late static binding.
2 — Constants
Class constants are values that are fixed at compile time and never change.
They are declared with the const keyword and accessed via
::. Unlike static properties they cannot be reassigned —
they're truly immutable.
class HttpStatus { // Constants are SCREAMING_SNAKE_CASE by convention public const int OK = 200; public const int CREATED = 201; public const int NO_CONTENT = 204; public const int BAD_REQUEST = 400; public const int UNAUTHORIZED = 401; public const int NOT_FOUND = 404; public const int UNPROCESSABLE_ENTITY = 422; public const int INTERNAL_SERVER_ERROR = 500; // Group related constants into an array constant (PHP 5.6+) public const array SUCCESS_CODES = [200, 201, 204]; } // Access without an instance: echo HttpStatus::NOT_FOUND; // 404 // Type-safe status checks: if ($response->getStatus() === HttpStatus::UNAUTHORIZED) { redirectToLogin(); } // Inside a method — use self::CONSTANT class ApiClient { private const string BASE_URL = 'https://api.example.com/v2'; private const int TIMEOUT_SEC = 30; public function get(string $path): array { return $this->http->request( self::BASE_URL . $path, ['timeout' => self::TIMEOUT_SEC] ); } }
Interface constants
Interfaces can also declare constants, and any class implementing the interface inherits them. This is a clean way to share domain-level constants without coupling to a concrete class.
interface HasRoles { public const string ROLE_ADMIN = 'admin'; public const string ROLE_EDITOR = 'editor'; public const string ROLE_VIEWER = 'viewer'; public function hasRole(string $role): bool; } class User implements HasRoles { public function __construct(private array $roles) {} public function hasRole(string $role): bool { return in_array($role, $this->roles, true); } } $user = new User([HasRoles::ROLE_EDITOR]); var_dump($user->hasRole(HasRoles::ROLE_ADMIN)); // bool(false) var_dump($user->hasRole(HasRoles::ROLE_EDITOR)); // bool(true)
PHP 8.1 Enums — the modern constant group
PHP 8.1 introduced native enums, which replace many patterns
that previously used class constants. Where you once wrote
const STATUS_PENDING = 'pending', you now write
a proper type-safe enum.
enum OrderStatus: string { case Pending = 'pending'; case Paid = 'paid'; case Shipped = 'shipped'; case Cancelled = 'cancelled'; public function label(): string { return match($this) { self::Pending => 'Awaiting payment', self::Paid => 'Payment received', self::Shipped => 'On the way', self::Cancelled => 'Cancelled', }; } public function isFinal(): bool { return $this === self::Shipped || $this === self::Cancelled; } } class Order { private OrderStatus $status = OrderStatus::Pending; public function ship(): void { if ($this->status !== OrderStatus::Paid) { throw new LogicException('Can only ship paid orders'); } $this->status = OrderStatus::Shipped; } } echo OrderStatus::Shipped->label(); // "On the way" var_dump(OrderStatus::Paid->isFinal()); // bool(false) // Type safety — this is now a compile error, not a runtime surprise: // $order->ship(); // works // $order->status = 'invalid'; // Fatal: type mismatch
3 — self:: vs static::: Late Static Binding
This is one of the most misunderstood PHP features — and one that bites developers regularly when using factory methods in inheritance hierarchies.
self:: always refers to the class where the method is physically written,
determined at compile time.
static:: refers to the class that was actually called at runtime — the
"late" in "late static binding" means the binding happens at call time, not compile time.
class Model { protected static string $table = 'models'; // ── Using self:: ────────────────────────────────────────── public static function findWithSelf(int $id): static { echo "SELECT * FROM " . self::$table; // self:: always resolves to Model — even when called on User! return new self(); // Always creates a Model, not the subclass } // ── Using static:: ──────────────────────────────────────── public static function find(int $id): static { echo "SELECT * FROM " . static::$table; // static:: resolves to whichever class was actually called return new static(); // Creates the correct subclass } } class User extends Model { protected static string $table = 'users'; // override the table name } class Post extends Model { protected static string $table = 'posts'; } // ── Calling via a subclass ──────────────────────────────── User::findWithSelf(1); // Prints: "SELECT * FROM models" ← WRONG (self:: locked to Model) User::find(1); // Prints: "SELECT * FROM users" ← CORRECT (static:: resolved at runtime) Post::find(2); // Prints: "SELECT * FROM posts" ← CORRECT // Also: new static() returns the correct subclass type $user = User::find(1); echo get_class($user); // "User" — not "Model"
Quick reference
self::— the class the method is written in (compile time)static::— the class the method is called on (runtime)parent::— the immediate parent classnew static()— creates an instance of whatever class was callednew self()— always creates the class where the code lives
Rule: in factory/clone methods that might be inherited, always use
static:: and new static().
Use self:: only when you specifically want to lock behaviour
to the declaring class, regardless of subclassing.
4 — The Singleton & Factory Patterns
These are the two most common real-world uses of static.
You'll encounter both in framework source code constantly.
Singleton — one instance, globally accessible
The Singleton pattern ensures a class has exactly one instance and provides a global access point to it. Classic use-cases: database connections, configuration, application registry. Use sparingly — singletons are notoriously hard to test. Prefer dependency injection instead when possible.
class Config { // Holds the single instance — null until first call private static ?self $instance = null; private array $data = []; // Private constructor — nobody can call new Config() private function __construct() {} // Private clone — prevent copying the singleton private function __clone() {} public static function getInstance(): static { if (static::$instance === null) { static::$instance = new static(); } return static::$instance; } public function set(string $key, mixed $value): void { $this->data[$key] = $value; } public function get(string $key, mixed $default = null): mixed { return $this->data[$key] ?? $default; } } // Always returns the same object: Config::getInstance()->set('debug', true); $debug = Config::getInstance()->get('debug'); // true — same instance
Factory pattern — decouple creation from use
A Factory creates objects without the caller needing to know the concrete class. The caller asks for "a payment gateway" — the factory decides whether to give Stripe, PayPal, or a test double.
class NotificationFactory { // Returns correct subclass based on type — caller doesn't need to know which public static function create(string $type, string $recipient, string $message): Notification { return match($type) { 'email' => new EmailNotification($recipient, $message, 'Notification'), 'sms' => new SmsNotification($recipient, $message, $recipient), 'push' => new PushNotification($recipient, $message), default => throw new InvalidArgumentException("Unknown type: {$type}"), }; } } // Consumer asks for a notification — doesn't care about the subclass: $channel = $user->preferredChannel(); // 'email', 'sms', or 'push' $notif = NotificationFactory::create($channel, $user->getEmail(), 'Order shipped!'); $notif->send(); // Switch user to SMS? Zero code changes in the consumer.
When static is the wrong choice
The pattern you should avoid: using static to avoid passing dependencies, creating hidden global state, or making classes that are impossible to swap for testing.
- ❌
DB::query(...)— hidden database coupling, impossible to test without a real DB - ❌
Log::error(...)— no way to capture logs in tests - ✅ Inject a
Loggerinterface and call$this->logger->error(...)— swappable, testable
15-Minute Review — Session 06
Switch the sidebar timer to review mode. No scrolling up — answer from memory.
Q1 — A static property is shared:
Q2 — What is the key difference between self:: and static::?
Q3 — In the Singleton pattern, why is the constructor made private?
Q4 — What is the difference between a class constant and a static property?
Q5 — In a named constructor like Color::fromHex(), why use new static() instead of new self()?