Objects Namespaces And Application Architecture

Static Members

Static methods are useful for named constructors, small stateless helpers, and class-level facts. Static properties are shared state, so use them carefully. Shared mutable state can make tests, long-running workers, and debugging harder.

Static Methods Do Not Need An Object

A static method can be called without creating an object.

PHP example
<?php

declare(strict_types=1);

class Slug
{
    public static function fromTitle(string $title): string
    {
        return trim(preg_replace('/[^a-z0-9]+/', '-', strtolower($title)) ?? '', '-');
    }
}

echo Slug::fromTitle('New Product Launch!') . PHP_EOL;

// Prints:
// new-product-launch

This works because the method does not need object state. It only depends on the input argument.

Static Methods Cannot Use $this

$this refers to a specific object. Static methods are not called on a specific object, so $this is not available.

PHP example
<?php

declare(strict_types=1);

class Vat
{
    public static function addStandardRate(int $netPennies): int
    {
        return (int) round($netPennies * 1.2);
    }
}

echo Vat::addStandardRate(1000) . PHP_EOL;

// Prints:
// 1200

If a method needs $this, it should usually be an instance method.

Static Named Constructors Can Improve Clarity

A static method can create an object in a named way.

PHP example
<?php

declare(strict_types=1);

class Money
{
    public function __construct(private int $pennies)
    {
    }

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

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

$money = Money::fromPounds(25);

echo $money->pennies() . PHP_EOL;

// Prints:
// 2500

Money::fromPounds(25) communicates the input unit better than new Money(2500).

Static Properties Are Shared

A static property has one value for the class, not one value per object.

PHP example
<?php

declare(strict_types=1);

class IdSequence
{
    private static int $nextId = 1;

    public static function next(): int
    {
        return self::$nextId++;
    }
}

echo IdSequence::next() . PHP_EOL;
echo IdSequence::next() . PHP_EOL;

// Prints:
// 1
// 2

This can be useful in tiny examples, but real applications usually get IDs from databases, UUID generators, or dedicated services.

Avoid Static Service Locators

Static access can hide dependencies. Code that calls Database::query() from anywhere is harder to test and reason about than code that receives a database connection or repository explicitly.

PHP example
<?php

declare(strict_types=1);

class ReferenceGenerator
{
    public static function orderReference(int $id): string
    {
        return 'ORD-' . str_pad((string) $id, 6, '0', STR_PAD_LEFT);
    }
}

echo ReferenceGenerator::orderReference(42) . PHP_EOL;

// Prints:
// ORD-000042

This static method is fine because it is deterministic and has no hidden external dependency.

Use self:: Inside The Same Class

Inside a class, use self:: to refer to static members on that class.

PHP example
<?php

declare(strict_types=1);

class OrderStatus
{
    private static array $paidStatuses = ['paid', 'settled'];

    public static function isPaid(string $status): bool
    {
        return in_array($status, self::$paidStatuses, true);
    }
}

echo OrderStatus::isPaid('paid') ? 'paid' : 'unpaid';
echo PHP_EOL;

// Prints:
// paid

Later lessons cover static:: and late static binding. For now, self:: is the direct way to refer to the current class.

What To Remember

Use static methods for stateless helpers, named constructors, and class-level facts. Be cautious with static properties because they are shared mutable state. If code needs configuration, a database, a clock, a logger, or an API client, passing a dependency is usually clearer than hiding it behind static access.

Static Means Class-Level, Not Automatically Stateless

A static method is called on a class rather than an instance. That can fit a pure operation whose result depends only on explicit arguments, such as parsing a well-defined value. It does not guarantee purity: a static method can read environment variables, mutate static properties, use the clock, or call a global container.

Review dependencies rather than syntax. If behavior needs a repository, HTTP client, logger, or configurable policy, an injected object usually makes those requirements clearer. A static call that reaches hidden global state is difficult to substitute in tests and difficult to configure differently for two consumers in one process.

Good Uses For Static Methods

Named constructors on immutable values are a common good use. DateRange::fromStrings() can validate and return a complete value while a private constructor protects canonical state. The method belongs to the type and needs no long-lived collaborator.

Small deterministic utility operations can also be static when they form a cohesive concept. Avoid a miscellaneous Utils class that accumulates unrelated formatting, filesystem, network, and business functions. Group behavior by domain responsibility and keep arguments and results explicit.

Static factories that select concrete infrastructure from environment variables are less attractive. Selection belongs in the composition root so production, test, CLI, and worker processes can wire dependencies deliberately.

Static Properties Are Process State

A static property is shared by all instances in one PHP process. Under traditional PHP-FPM, that state usually lasts for one request, but tests, queue workers, application servers, and coroutine runtimes can keep the process alive across many operations. Data left by one job can affect another.

Common hazards include mutable configuration caches, current-user state, tenant identifiers, request-scoped memoization, and counters assumed to reset automatically. A test suite may also pass or fail depending on execution order if static state is not restored.

Static state is not shared across processes unless backed by an external system. Incrementing a static counter does not produce a cluster-wide sequence, and a static cache does not invalidate values held by other workers. Name process-local behavior honestly and do not use it for coordination.

Constants And Immutable Shared Data

Class constants are appropriate for values fixed with the code, such as supported state names or protocol field labels. Configuration that differs by deployment is not a constant merely because it is convenient to access statically.

An immutable object can be shared through dependency injection without becoming global. This provides one validated configuration instance while keeping its use visible in constructors. It also allows a test to supply another instance without resetting class state.

self, static, And Late Static Binding

self:: refers to the class where the method is defined. static:: uses late static binding and can resolve to the called subclass. The distinction matters in inheritable factories and hooks.

A base value class that returns new self(...) always creates the base class. Returning new static(...) may create a child, but that assumes every child constructor and invariant remain compatible. Before using late static binding, decide whether subclass creation is a supported feature. Often a final class with explicit factories is safer.

static return types can describe fluent or factory methods that return the called class, while self describes the declaring class. Type syntax helps callers, but it cannot make an unsafe inheritance design substitutable.

Avoid Static Service Locators

A static container call hides the object graph:

PHP example
$gateway = Container::get(PaymentGateway::class);

The surrounding class now depends on a payment gateway and on the global container, although neither appears in its constructor. Tests must mutate global registrations, and concurrent or nested operations cannot easily use different configurations.

Inject the gateway through the constructor. Keep the container at application entry points where it constructs controllers, commands, and workers. Framework facades may provide concise syntax, but reviewers should understand whether they are proxies to request-scoped services or mutable global state.

Caching Inside Static Methods

A static local or property is sometimes used to memoize an expensive deterministic result. Define the cache key, lifetime, memory bound, and invalidation rule. In a long-running process, a map keyed by unbounded user input is a memory leak.

Memoization is safe only when repeated calls with the same key should return the same result for the entire cache lifetime. Results based on locale, tenant, permissions, time, or mutable configuration need those dimensions in the key or should not use process-global memoization.

Expose a reset only when the lifecycle genuinely requires it, and do not rely on tests calling reset while production never does. A scoped cache object can make lifetime and capacity clearer.

Testing Static Behavior

Pure static methods are easy to test with input-output examples and property tests. Cover malformed input, boundary values, and canonicalization. There is no need to mock a deterministic parser merely because it is static.

For static properties, run several operations in one process and vary order. Test two tenants or jobs sequentially and prove no state crosses the boundary. Parallel test runners use separate processes, so they may hide process-local leaks unless one test intentionally performs repeated work.

Static calls to time, randomness, filesystem, or network APIs are harder to control. Wrap the external capability behind an injected collaborator at the boundary where deterministic tests and failure handling matter.

Refactoring A Static Dependency

When removing a widely used static service, introduce an instance service with an explicit interface where a real boundary exists. Wire it into one application path, migrate callers, then remove the global access after usage reaches zero. Avoid keeping both indefinitely, because new code will continue choosing the shorter global path.

A static compatibility wrapper can delegate temporarily to an instance, but it still needs a global registration and should carry a deprecation plan. Measure use and make the migration visible in code review.

Review Checklist

Ask whether the method is deterministic from its arguments, whether any static property can outlive a request, whether the data is shared across processes, whether late static binding is intentional, and whether a global lookup hides a collaborator. Inspect worker and test lifecycles, not only one HTTP request.

Concurrency Makes Hidden State More Visible

Event-loop servers, fibers, and coroutine frameworks can interleave work inside one process. A static current request, locale, or tenant may change while another operation is suspended, causing one caller to observe another caller's context. Code written under a one-request-per-process assumption must be reviewed before moving to a persistent concurrent runtime.

Pass request context explicitly or use a framework-supported scoped context whose isolation guarantees are documented. Even then, domain services should receive the specific identity or policy they need instead of reaching into ambient context. Test overlapping operations with different tenants and force suspension between setting and reading context. Sequential unit tests cannot expose this class of leak.

After this lesson, you should be able to distinguish class-level syntax from stateless behavior, use static named constructors and pure operations appropriately, avoid hidden service locators, reason about process-local static state, apply late static binding deliberately, and test repeated operations in long-running PHP processes.

Practice

Task: Create Order References

Create a small class for generating order references.

Requirements

  • Use declare(strict_types=1);.
  • Create an OrderReference class.
  • Add a static method that accepts a positive integer order ID.
  • Reject IDs lower than 1.
  • Return references in the format ORD-000123.
  • Print two valid references.
  • Show one invalid ID by catching the exception.
  • Include the expected output as comments in the same PHP code block.

The method should be static because it is deterministic and does not need object state.

Show solution
PHP example
<?php

declare(strict_types=1);

class OrderReference
{
    public static function fromId(int $id): string
    {
        if ($id < 1) {
            throw new InvalidArgumentException('Order ID must be positive.');
        }

        return 'ORD-' . str_pad((string) $id, 6, '0', STR_PAD_LEFT);
    }
}

echo OrderReference::fromId(42) . PHP_EOL;
echo OrderReference::fromId(123) . PHP_EOL;

try {
    OrderReference::fromId(0);
} catch (InvalidArgumentException $exception) {
    echo $exception->getMessage() . PHP_EOL;
}

// Prints:
// ORD-000042
// ORD-000123
// Order ID must be positive.

The method is static because it only transforms an input value into a reference string. It has no object state and no hidden external dependency.