Static Methods & Properties
debt(d5/e7/b7/t7)
Closest to 'specialist tool catches it' (d5). The detection_hints list PHPStan, Mockery, and PHPUnit — PHPStan can flag static calls in business logic and note that statics cannot be mocked, but this requires deliberate configuration and a specialist SAST mindset. Standard linters don't flag static usage by default; a developer must run PHPStan with appropriate rules. The problem is silent at runtime until tests fail or concurrency issues surface.
Closest to 'cross-cutting refactor across the codebase' (e7). The quick_fix says 'Replace static utility methods with injected service objects,' but this is not a single-line fix — it requires introducing constructor injection, updating every call site, wiring dependency injection containers, and modifying tests. Because static calls can be spread across many classes and layers (web, cli, queue-worker all listed in applies_to), removal is a cross-cutting refactor. Slightly below e9 because it doesn't require a full architectural rewrite, but it comfortably exceeds multi-file e5.
Closest to 'strong gravitational pull' (b7). Static methods apply across web, cli, and queue-worker contexts, meaning every new feature or test written against a statically-coupled class inherits the coupling. The tags include coupling and testing, and the definition notes hidden global dependencies. Each new developer and each new class that calls a static method adds another point of coupling, shaping how the whole codebase is structured around testability. Not quite b9 (the entire system is not redefined), but the gravitational pull is strong and pervasive.
Closest to 'serious trap — contradicts how a similar concept works elsewhere' (t7). The misconception field explicitly states that developers believe static methods are 'just regular methods that do not need instantiation,' when in fact they create hidden dependencies, cannot be mocked by standard test doubles, and cause test pollution via shared state. This directly contradicts OOP intuitions from instance-based design (where swapping an implementation is straightforward). The trap is well-documented but consistently surprises developers migrating from procedural backgrounds or using Laravel Facades, earning t7.
Also Known As
TL;DR
Explanation
Static methods and properties belong to the class rather than an instance and are called with :: (ClassName::method()). They are appropriate for: pure utility functions (Math::clamp()), named constructors/factories (Money::fromFloat()), and singleton registries. However, static state is global state — it persists across test cases, cannot be easily mocked, and creates hidden dependencies. Static calls are hard to stub in tests without workarounds like Mockery's alias mocking. Prefer instance methods with dependency injection for anything stateful or replaceable; reserve static for genuinely stateless utilities.
Common Misconception
Why It Matters
Common Mistakes
- Using static methods for stateful operations — global state in static methods causes test pollution.
- Static factory methods that return a concrete type — cannot be overridden in subclasses without LSP violation.
- Calling static methods on $this inside a class — use self:: or static:: explicitly.
- Not distinguishing stateless utility statics (Math::round()) from stateful service statics (DB::query()) — the former is fine, the latter is problematic.
Code Examples
// Static method with global state — cannot be unit tested:
class Database {
private static ?PDO $connection = null;
public static function query(string $sql): array {
return self::getConnection()->query($sql)->fetchAll();
}
}
// Callers: Database::query() — tightly coupled, untestable without real DB
// Static methods: no instance state, utility/factory use cases
class Uuid {
public static function v4(): string {
return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
}
}
$id = Uuid::v4();
// Named constructors — static factory on a value object
readonly class Temperature {
private function __construct(public float $celsius) {}
public static function fromCelsius(float $c): self { return new self($c); }
public static function fromFahrenheit(float $f): self { return new self(($f-32)*5/9); }
public static function fromKelvin(float $k): self { return new self($k-273.15); }
}