Mid-level 5 min · March 06, 2026

PHP Static Singleton — Cache Contaminated by Test Order

A static property $userCache persisted across tests, causing order-dependent failures.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Static members belong to the class, not to objects — shared across all instances.
  • self:: resolves to the class where the code is written; static:: resolves to the child class at runtime via Late Static Binding.
  • Legitimate uses: pure utility functions, named constructors, singletons when genuinely needed.
  • Avoid static for dependencies that need swapping in tests — use dependency injection.
  • Performance: static calls are ~5% faster than instance calls due to no constructor overhead, but the difference rarely matters at scale.
Plain-English First

Imagine a scoreboard at a basketball game. Every player on the court is their own person (their own object), but there's only ONE scoreboard that everyone shares. You don't need to ask a specific player what the score is — the scoreboard belongs to the game itself. Static properties and methods work exactly like that scoreboard: they belong to the class itself, not to any individual object created from it.

Most PHP developers learn static methods early and then either overuse them everywhere or avoid them entirely out of confusion. Neither extreme is right. Static members are one of PHP's most misunderstood features — powerful in the right context, a liability in the wrong one. Understanding them deeply separates developers who write maintainable code from those who create tightly-coupled spaghetti.

The problem static solves is straightforward: sometimes data or behaviour genuinely belongs to a concept (the class), not to any specific instance of it. A database connection counter, a registry of loaded plugins, a utility method that formats a currency string — none of these need an object to exist first. Forcing them into instance methods means creating throwaway objects just to call a function, which wastes memory and communicates the wrong intent to anyone reading your code.

By the end of this article you'll know exactly how static properties and methods work under the hood in PHP, the difference between self:: and static:: (this one trips up senior devs), the legitimate real-world patterns where static shines, and the exact pitfalls that turn static code into a testing nightmare. You'll be able to make deliberate, defensible choices — not guesses.

What Static Actually Means — Class-Level vs Instance-Level

Every time you call new User() in PHP, the engine allocates fresh memory for that object and populates its properties with their default values. That object is completely independent — changing $userA->name has zero effect on $userB->name. This is instance-level state, and it's the backbone of OOP.

Static is the opposite deal. A static property lives in the class definition itself, not inside any object. PHP allocates it exactly once for the lifetime of the request, and every object of that class — plus any code that references the class directly — shares the exact same value. There's no copying, no per-object version.

A static method is similar: it's a function attached to the class rather than to an instance. Because there's no instance involved, PHP won't give you a $this variable inside a static method. Trying to use $this in a static context is a fatal error. Instead, you use self:: or static:: to reference the class itself.

This distinction isn't just academic. It changes how you reason about your code. Instance methods say 'do this TO an object'. Static methods say 'do this WITH this class'. Getting that mental model right is half the battle.

PageVisitCounter.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php

class PageVisitCounter
{
    // This property belongs to the CLASS, not to any single object.
    // All instances share this one value.
    private static int $totalVisits = 0;

    // A regular instance property — each object gets its own copy.
    private string $pageName;

    public function __construct(string $pageName)
    {
        $this->pageName = $pageName;

        // Every time any page object is created, the shared counter increments.
        self::$totalVisits++;
    }

    // Static method — no $this, because no specific object is needed.
    public static function getTotalVisits(): int
    {
        return self::$totalVisits;
    }

    // Regular instance method — tied to THIS specific page object.
    public function getPageName(): string
    {
        return $this->pageName;
    }
}

// Three separate page objects are created...
$homepage   = new PageVisitCounter('Home');
$aboutPage  = new PageVisitCounter('About');
$contactPage = new PageVisitCounter('Contact');

// ...but there is only ONE shared counter across all of them.
echo PageVisitCounter::getTotalVisits() . PHP_EOL; // 3

// You can also call it via an instance — PHP allows this, but it's misleading style.
// Prefer the class-name syntax above to signal it's static.
echo $homepage->getTotalVisits() . PHP_EOL; // Still 3 — same counter

echo $homepage->getPageName() . PHP_EOL;   // Home
echo $aboutPage->getPageName() . PHP_EOL;  // About
Output
3
3
Home
About
Watch Out:
Static properties persist for the entire PHP request lifecycle. In a long-running process (like a ReactPHP server or a Laravel queue worker), a static counter that you expect to reset between 'requests' will NOT reset — it'll just keep climbing. This is a real source of bugs in modern PHP applications.
Production Insight
Static properties are a primary cause of memory bloat in long-running PHP processes.
Each request in a traditional mod_php lifecycle is isolated — but in ReactPHP or Swoole, static state accumulates.
Rule: never use static properties for cumulative counters in long-lived processes unless you explicitly reset them.
Key Takeaway
Static = class-level, shared across all instances for the full request.
Instance = per-object, isolated.
The two worlds never cross — $this is illegal in static methods.

self:: vs static:: — The Late Static Binding Trap

Here's where most intermediate developers have a gap in their knowledge: self:: and static:: look almost identical but behave completely differently when inheritance is involved.

self:: is resolved at compile time. It always refers to the class where the method was physically written, regardless of which child class called it. Think of it as hard-coded — it doesn't care about the runtime context.

static:: uses Late Static Binding (LSB), which means PHP resolves it at runtime to whichever class actually triggered the call. If a child class calls an inherited static method, static:: will point to the child class, not the parent.

This matters enormously in factory methods and singleton patterns. If you write a base Model class with a static::create() factory and use self::, every subclass's create() will silently return a base Model object instead of the correct subclass. That bug is invisible until you try to call a child-class method on the returned object.

The rule of thumb: use static:: in any static method you expect subclasses to inherit. Use self:: only when you intentionally want to lock the reference to the current class — for example, in a constant lookup where inheritance shouldn't change the value.

ModelFactory.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php

class BaseModel
{
    protected string $type;

    public function __construct()
    {
        // get_class($this) shows us which class was actually instantiated.
        $this->type = get_class($this);
    }

    // BAD version — uses self:: which is locked to BaseModel at compile time.
    public static function createWithSelf(): static
    {
        return new self(); // Always creates a BaseModel, even when called on a child!
    }

    // GOOD version — uses static:: which resolves to the calling class at runtime.
    public static function createWithStatic(): static
    {
        return new static(); // Creates whichever class actually called this method.
    }

    public function getType(): string
    {
        return $this->type;
    }
}

class UserModel extends BaseModel
{
    public function fetchPermissions(): string
    {
        return "Fetching permissions for a {$this->type}";
    }
}

// --- Demonstrating the difference ---

$selfResult   = UserModel::createWithSelf();   // Calls BaseModel::createWithSelf()
$staticResult = UserModel::createWithStatic(); // Calls BaseModel::createWithStatic()

echo $selfResult->getType() . PHP_EOL;   // BaseModel — WRONG! We called UserModel.
echo $staticResult->getType() . PHP_EOL; // UserModel  — Correct.

// This will cause a fatal error because $selfResult is a BaseModel, not a UserModel.
// Uncomment to see: echo $selfResult->fetchPermissions();

// This works perfectly because $staticResult IS a UserModel.
echo $staticResult->fetchPermissions() . PHP_EOL;
Output
BaseModel
UserModel
Fetching permissions for a UserModel
Interview Gold:
Late Static Binding was introduced in PHP 5.3 specifically to fix the self:: inheritance problem. If an interviewer asks 'what is late static binding?', the answer is: it's PHP's mechanism for deferring the resolution of static:: to runtime rather than compile time, so inherited static methods correctly reflect the child class that called them.
Production Insight
A real incident: a base Repository class used self:: in a find() method. When a child UserRepository inherited it, all find() calls returned Repository objects instead of UserRepository objects. The bug went unnoticed for two weeks because only Repository methods were used. When UserRepository added a custom method, production crashes followed.
Rule: always use static:: in methods that are intended to be overridden or extended.
Key Takeaway
self:: = compile-time, hard-coded to the class where the method is written.
static:: = runtime, resolves to the calling class.
If you want inheritance to work correctly, use static::.

Two Legitimate Real-World Patterns for Static in PHP

Static gets a bad reputation largely because it's overused. But there are genuine, well-established patterns where it's the right tool.

Pattern 1 — The Singleton (use sparingly): When your application genuinely needs exactly one instance of something — a logger, a config loader, a database connection pool — the Singleton pattern uses a static property to hold that single instance and a static method to retrieve it. The static property ensures only one instance is ever stored, regardless of how many times you call getInstance().

Pattern 2 — Named Constructors / Static Factory Methods: PHP constructors are limited: you can only have one __construct(). Static factory methods solve this elegantly. Money::fromCents(150) and Money::fromFloat(1.50) are far clearer than trying to overload a constructor with optional parameters and type checks. Each factory method is static because you need to call it before any object exists yet.

Both patterns are about clarity of intent. When you see Logger::getInstance(), you instantly know there's one logger. When you see DateRange::fromString('2024-01-01/2024-12-31'), you know exactly what kind of construction is happening. Static, used this way, makes your API more expressive — not less maintainable.

MoneyValue.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<?php

/**
 * Money value object demonstrating static factory methods.
 * Multiple ways to construct — all clear, all explicit.
 */
class Money
{
    // Private constructor forces callers to use the named factory methods.
    // This prevents ambiguous construction like: new Money(150, 'cents') vs new Money(1.50, 'dollars')
    private function __construct(
        private readonly int    $amountInCents,
        private readonly string $currencyCode
    ) {}

    // Factory method 1: construct from an integer number of cents.
    public static function fromCents(int $cents, string $currency = 'USD'): self
    {
        // Validate here before the object is even created — impossible in a single constructor.
        if ($cents < 0) {
            throw new InvalidArgumentException('Amount cannot be negative.');
        }
        return new self($cents, strtoupper($currency));
    }

    // Factory method 2: construct from a float (handles the cents conversion internally).
    public static function fromFloat(float $amount, string $currency = 'USD'): self
    {
        if ($amount < 0.0) {
            throw new InvalidArgumentException('Amount cannot be negative.');
        }
        // Round to avoid floating-point dust (e.g., 1.005 * 100 = 100.4999...)
        $cents = (int) round($amount * 100);
        return new self($cents, strtoupper($currency));
    }

    // Factory method 3: construct from a formatted string like "$9.99".
    public static function fromFormattedString(string $formattedAmount, string $currency = 'USD'): self
    {
        // Strip currency symbols and whitespace before parsing.
        $cleaned = preg_replace('/[^0-9.]/', '', $formattedAmount);
        return self::fromFloat((float) $cleaned, $currency);
    }

    public function getAmountInCents(): int
    {
        return $this->amountInCents;
    }

    public function format(): string
    {
        return sprintf('%s %.2f', $this->currencyCode, $this->amountInCents / 100);
    }

    // A pure utility — doesn't need instance state, belongs to the concept of Money.
    public static function zero(string $currency = 'USD'): self
    {
        return new self(0, strtoupper($currency));
    }
}

// --- Three crystal-clear ways to create Money objects ---

$productPrice   = Money::fromCents(1999);             // $19.99 from a database integer
$shippingFee    = Money::fromFloat(4.99);              // $4.99 from user input
$discountAmount = Money::fromFormattedString('$2.50'); // $2.50 from a config file
$emptyWallet    = Money::zero();

echo $productPrice->format()   . PHP_EOL;
echo $shippingFee->format()    . PHP_EOL;
echo $discountAmount->format() . PHP_EOL;
echo $emptyWallet->format()    . PHP_EOL;

echo 'Product in cents: ' . $productPrice->getAmountInCents() . PHP_EOL;
Output
USD 19.99
USD 4.99
USD 2.50
USD 0.00
Product in cents: 1999
Pro Tip:
Static factory methods with descriptive names are one of the cleanest API design patterns in PHP. Libraries like Carbon (Carbon::now(), Carbon::parse()) and Laravel's Eloquent use this heavily. Making your constructor private and exposing only named factories forces callers to be explicit about HOW they're building the object — which eliminates a whole class of bugs.
Production Insight
Singletons create hidden coupling: every class that calls Logger::getInstance() is tethered to that one logger. If you later need a different logger for a subsystem, you have to modify Logger itself or add an environment check.
Rule: use singletons only when the cost of passing an instance via DI exceeds the coupling pain — and even then, consider a service container.
Key Takeaway
Static patterns are good for: pure utilities, named constructors, and genuine singletons.
They are bad for: any dependency that changes per context.
If a test would need to swap it, don't make it static.

Why Static Can Hurt You — Testability and Hidden State

Static is seductive because it's convenient. DatabaseConnection::query($sql) is easier to type than injecting a $db dependency everywhere. But convenience now often means pain later, and static is one of the most common sources of untestable code in PHP projects.

The core problem: static calls are hard-coded dependencies. When OrderProcessor calls TaxCalculator::calculate($amount) directly, you cannot swap TaxCalculator for a test double without either modifying OrderProcessor or reaching for PHP-specific mocking tricks that only work in specific test frameworks. Unit tests should be isolated — that's their entire value.

Static properties compound this by introducing hidden global state. A test that runs fine in isolation can fail when run after another test that left a static property in a modified state. These bugs are notoriously hard to track down.

The litmus test for static is simple: Is this behaviour or data genuinely context-free? A string formatter that trims whitespace and capitalises words needs no context — static is fine. An order processor that calculates prices needs a tax strategy, a discount service, and locale awareness — those are dependencies that should be injected, not statically called. When you're unsure, ask 'do I need to swap this out in a test?' If yes, don't use static.

StringHelper.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php

/**
 * A utility class whose methods are genuinely context-free.
 * These are good static candidates — they take input, return output,
 * hold no state, and never need to be swapped for a test double.
 */
class StringHelper
{
    // Pure function: same input always gives same output, no side effects.
    public static function toTitleCase(string $sentence): string
    {
        // ucwords converts first letter of each word to uppercase.
        return ucwords(strtolower(trim($sentence)));
    }

    // Pure function: strips non-alphanumeric characters for use in URLs.
    public static function toSlug(string $title): string
    {
        $lowercased  = strtolower(trim($title));
        $spaceless   = preg_replace('/[\s_]+/', '-', $lowercased);  // spaces and underscores become hyphens
        $clean       = preg_replace('/[^a-z0-9\-]/', '', $spaceless); // strip everything else
        return rtrim($clean, '-'); // remove any trailing hyphens
    }

    // Pure function: truncates a string and appends ellipsis if needed.
    public static function truncate(string $text, int $maxLength = 100, string $suffix = '...'): string
    {
        if (mb_strlen($text) <= $maxLength) {
            return $text; // Already short enough — return as-is.
        }

        // Cut at a word boundary so we don't slice mid-word.
        $truncated = mb_substr($text, 0, $maxLength);
        $lastSpace = mb_strrpos($truncated, ' ');

        return ($lastSpace !== false)
            ? mb_substr($truncated, 0, $lastSpace) . $suffix
            : $truncated . $suffix;
    }
}

// These static calls are totally fine — no hidden state, fully testable as pure functions.
$articleTitle   = StringHelper::toTitleCase('  the QUICK brown FOX  ');
$urlSlug        = StringHelper::toSlug('The Quick Brown Fox! (2024)');
$previewText    = StringHelper::truncate(
    'Static methods in PHP can be extremely useful when applied thoughtfully to the right problems.',
    50
);

echo $articleTitle . PHP_EOL;
echo $urlSlug      . PHP_EOL;
echo $previewText  . PHP_EOL;
Output
The Quick Brown Fox
the-quick-brown-fox-2024
Static methods in PHP can be extremely useful...
Watch Out:
Never use static properties as a substitute for dependency injection just because 'it's easier to access'. Config::get('db.host') sounds clean, but if Config stores loaded values in a static property, every test that touches Config now shares state. The fix is to use a proper DI container and inject configuration objects — Laravel's service container, Symfony's DIC, or even a simple constructor injection.
Production Insight
The PHP mocking library Mockery can mock static methods with shouldReceive, but this replaces the class globally — it's stateful and can leak between tests. A better approach is to refactor to instance methods and use constructor injection.
Rule: if you need Mockery::mock('alias:MyClass') to test, your design is fighting you.
Key Takeaway
Static calls create hard-coded dependencies.
Static properties create hidden global state.
Both make unit testing painful and flaky.
The fix: dependency injection for services; static only for pure functions and genuine constants.

Refactoring Static Dependencies: A Practical Migration Guide

A common real-world scenario: you inherit a legacy PHP codebase where half the business logic lives in static methods. OrderService::calculateTotal($order) is called from controllers, commands, and even other static utilities. Your task is to make OrderService testable without rewriting the entire application in one go.

The safe migration strategy is the Tuckman Refactoring Pattern:

  1. Wrap the static call in a non-static wrapper class that delegates to the static method.
  2. Implement the wrapper as an interface, and create a production implementation that calls the static method.
  3. Replace calls to the static method with calls through the wrapper interface.
  4. Inject the wrapper via constructor or setter method.
  5. Replace the static implementation with an instance implementation once all callers use the interface.

This approach lets you swap the static logic without changing every consumer at once. The wrapper class becomes the seam where you can inject a test double.

In modern PHP frameworks, the service container handles this cleanly. Laravel's App::bind() or Symfony's $container->set() let you rebind the implementation in tests. The static call becomes a one-line binding change instead of a month-long refactor.

StaticRefactor.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?php

// Step 1: Define an interface for the service.
interface OrderTotalCalculatorInterface
{
    public function calculate(Order $order): Money;
}

// Step 2: Create a wrapper that delegates to the existing static method.
class LegacyOrderTotalCalculator implements OrderTotalCalculatorInterface
{
    public function calculate(Order $order): Money
    {
        // Delegate to the static method (temporarily).
        return OrderService::calculateTotal($order);
    }
}

// Step 3: In your controller, inject the interface instead of calling static.
class OrderController
{
    public function __construct(
        private OrderTotalCalculatorInterface $calculator
    ) {}

    public function show(int $orderId): array
    {
        $order = Order::findOrFail($orderId);
        $total = $this->calculator->calculate($order);
        return ['total' => $total->format()];
    }
}

// Step 4: In a test, bind a mock implementation.
class OrderTest extends TestCase
{
    public function test_total_is_calculated(): void
    {
        $mock = $this->createMock(OrderTotalCalculatorInterface::class);
        $mock->method('calculate')->willReturn(Money::fromCents(1000));

        app()->instance(OrderTotalCalculatorInterface::class, $mock);

        $response = $this->get('/orders/1');
        $response->assertJson(['total' => 'USD 10.00']);
    }
}
Refactoring Tip:
When wrapping a static call, keep the wrapper thin. It should contain no logic — only delegation. All the complexity stays in the static method until you're ready to move it into a proper instance class. This minimises regression risk.
Production Insight
Refactoring static dependencies often reveals hidden coupling. If OrderService::calculateTotal also calls TaxService::getRate(), you'll need to wrap that too. This is why static-heavy codebases feel rigid — the static binds everything together.
Rule: each wrapper you introduce is a seam. Use them to gradually decouple the system.
Key Takeaway
Wrap static calls in interfaces, then inject them.
Replace the wrapper implementation when you're ready.
This turns an impossible refactor into a safe, incremental migration.
● Production incidentPOST-MORTEMseverity: high

Static Singleton Cache Contaminated by Test Order

Symptom
All tests pass individually, but when the full test suite runs, one test fails intermittently with an assertion that a user's name is outdated. The failure order changes with every run.
Assumption
Each test starts with a clean database and no shared state. The team assumed since they used an in-memory SQLite database, there was no cross-test contamination.
Root cause
A UserRepository static property self::$userCache cached user data during one test and was never reset. Subsequent tests that queried the same user ID received stale data from the cache instead of the fresh database state.
Fix
Added a resetStaticProperties() method in the test base class called in setUp() that nulled out all relevant static caches. Better solution: replaced the static cache with an instance-level cache injected via constructor so each test gets a fresh instance.
Key lesson
  • Static properties persist across test executions — always reset them in setUp or tearDown.
  • Static state is the leading cause of flaky tests in PHP codebases.
  • Prefer instance-scoped caching when the container can manage lifecycle.
Production debug guideTrace and isolate problems caused by shared static properties3 entries
Symptom · 01
Test fails only when run in a specific order
Fix
Check static properties in shared classes — use var_dump() at the start of each test to see if property holds residual state from previous test. Run phpunit --order-by=defects to isolate.
Symptom · 02
Singleton instance returns stale data in a long-running process
Fix
Verify that the singleton lives across multiple requests when using ReactPHP, Swoole, or Amp. Use echo spl_object_id($instance) to confirm same object reused. Reset singleton after each job if volatile.
Symptom · 03
Factory method returns a parent object instead of a child class
Fix
Inspect the factory method — if it uses new self(), replace with new static(). The bug is invisible until you call a child-only method on the returned value.
★ Quick Debug: static:: vs self:: and Static State IssuesUse these commands and checks to quickly isolate static-related bugs.
Factory returns wrong class (expected child but got parent)
Immediate action
Check if the factory method uses `self::` or `static::`
Commands
grep -rn 'new self(' vendor/project/src
Replace with `new static()` where inheritance is expected
Fix now
Change self:: to static:: in all factory methods that subclasses override.
Static property value is unexpected across tests+
Immediate action
Add `var_dump()` of the static property at the start of the failing test
Commands
ReflectionClass::setStaticPropertyValue('propertyName', null);
phpunit --debug to see execution order
Fix now
Reset all relevant static properties in setUp() or tearDown() — or better, remove static state entirely.
Static vs Instance Methods in PHP
AspectStaticInstance
Belongs toThe class itselfA specific object instance
Access syntaxClassName::method() or self::/static::$this->method()
$this available?No — fatal error if usedYes — always available
Memory allocationOnce per request (shared)Once per object created
State persistenceEntire request lifetimeUntil object is garbage-collected
TestabilityHarder — tight coupling to class nameEasy — inject a mock/stub
Best forPure utilities, factories, singletonsBehaviour that depends on object state
Inheritance behaviourNeeds static:: for correct LSBWorks naturally via polymorphism
Override in child classAllowed but $this unavailableAllowed and $this works normally

Key takeaways

1
Static properties are shared across ALL instances of a class for the entire request
they're class-level, not object-level. Mutating a static property from any one place mutates it everywhere.
2
Use static:
instead of self:: in any static method that subclasses will inherit — self:: is locked to the class where the code was written, while static:: resolves to whichever class actually called the method at runtime.
3
The best use cases for static are genuinely context-free pure utility methods and named constructor factory patterns
not as a lazy alternative to dependency injection for services that have real dependencies.
4
Static state is the enemy of testable code. If a static property holds state that varies per-test, you'll get flaky tests that are hard to debug. The fix is almost always proper dependency injection with a DI container.

Common mistakes to avoid

3 patterns
×

Using self:: in inheritable factory methods

Symptom
Child class factory silently returns a parent class object; calling any child-only method causes a fatal error like 'Call to undefined method BaseModel::fetchPermissions()'
Fix
Replace self:: with static:: in any static method you intend child classes to inherit, so PHP resolves the class name at runtime via Late Static Binding.
×

Storing mutable application state in static properties

Symptom
Tests pass in isolation but fail when run together; values 'bleed' between test cases because static properties aren't reset between tests
Fix
Either reset static state in your test's tearDown() method, or — better — redesign the code to pass state via constructor injection so each test controls its own clean instance.
×

Calling static methods via an object instance ($obj::method() or $obj->method())

Symptom
Code works but colleagues are confused about whether the method is static or not; PHP E_DEPRECATED notice in strict environments
Fix
Always call static methods using the class name syntax (ClassName::method()) to make the intent unmistakable. This also ensures static analysis tools and IDEs correctly flag errors.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What is the difference between self:: and static:: in PHP, and when woul...
Q02SENIOR
You have a UserRepository class with a static $instances property used a...
Q03SENIOR
Can you call a non-static method statically in PHP? What happens, and ha...
Q01 of 03SENIOR

What is the difference between self:: and static:: in PHP, and when would using self:: cause a bug in a class hierarchy?

ANSWER
self:: is resolved at compile time and always refers to the class where the code is written, regardless of which child class called it. static:: uses Late Static Binding and resolves at runtime to the class that actually triggered the call. Bug scenario: if a base class factory method uses new self(), a child class calling that factory will get a base class object, not a child class object. This leads to fatal errors when calling child-specific methods on the returned object. Fix: use new static() instead.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
Can a static method in PHP access non-static properties?
02
What is the difference between self:: and static:: in PHP?
03
Is using static methods in PHP bad practice?
04
How do I refactor a large static method to be testable?
🔥

That's OOP in PHP. Mark it forged?

5 min read · try the examples if you haven't

Previous
Namespaces in PHP
6 / 7 · OOP in PHP
Next
Magic Methods in PHP