objects namespaces and application architecture

Late Static Bindings

self:: means "the class where this method is written." static:: means "the class that was used to make the call." That difference matters in inheritance, named constructors, and static factory methods.

self:: Binds Early

self::class refers to the class where the code is defined.

PHP example
<?php

declare(strict_types=1);

class BaseModel
{
    public static function selfName(): string
    {
        return self::class;
    }
}

final class UserModel extends BaseModel
{
}

echo UserModel::selfName() . PHP_EOL;

// Prints:
// BaseModel

Even though the method was called through UserModel, self::class still points to BaseModel.

static:: Binds Late

static::class uses the class that was actually called.

PHP example
<?php

declare(strict_types=1);

class BaseModel
{
    public static function staticName(): string
    {
        return static::class;
    }
}

final class UserModel extends BaseModel
{
}

echo UserModel::staticName() . PHP_EOL;

// Prints:
// UserModel

That is late static binding. PHP waits until runtime to decide what static:: means.

new static() In Named Constructors

Late static binding is often used in named constructors on base classes.

PHP example
<?php

declare(strict_types=1);

class Identifier
{
    public function __construct(
        public readonly string $value,
    ) {
        if ($value === '') {
            throw new InvalidArgumentException('Identifier cannot be empty.');
        }
    }

    public static function fromString(string $value): static
    {
        return new static(trim($value));
    }
}

final class OrderId extends Identifier
{
}

$id = OrderId::fromString('  ORD-123  ');

echo $id::class . ': ' . $id->value . PHP_EOL;

// Prints:
// OrderId: ORD-123

Because the method returns new static(), calling OrderId::fromString() returns an OrderId, not just an Identifier.

Constants And Static Properties

Late static binding can also affect constants and static properties.

PHP example
<?php

declare(strict_types=1);

class Report
{
    protected const TYPE = 'generic';

    public static function type(): string
    {
        return static::TYPE;
    }
}

final class SalesReport extends Report
{
    protected const TYPE = 'sales';
}

echo SalesReport::type() . PHP_EOL;

// Prints:
// sales

If Report::type() used self::TYPE, the result would be generic.

Risks Of new static()

new static() assumes child classes can be constructed the same way as the parent. That can break if a child class adds required constructor arguments.

PHP example
<?php

declare(strict_types=1);

class BaseRecord
{
    public function __construct(
        public string $id,
    ) {
    }

    public static function make(string $id): static
    {
        return new static($id);
    }
}

This is only safe if subclasses keep a compatible constructor. If subclasses need different construction rules, prefer explicit factory methods on each concrete class.

When To Use It

Late static binding is useful for:

  • inherited named constructors that should return the child class
  • base classes that expose overridden constants
  • framework base classes and active-record style models
  • fluent APIs that need to preserve the concrete type

It is not needed in most ordinary application services. If inheritance is not involved, static:: usually adds confusion without benefit.

What You Should Be Able To Do

After this lesson, you should be able to explain the difference between self:: and static::, understand why new static() can return a subclass, and recognise the constructor compatibility risk.

For junior work, the practical skill is reading framework and base-class code that uses static:: without confusing it with a normal static method call.

Practice

Practice: Compare `self::` And `static::`

Task

Build:

  • a base identifier class with selfName() and staticName()
  • a child identifier class
  • a named constructor that returns static

Print the result of calling both name methods through the child class, then create a child identifier through the inherited named constructor.

Use strict types. Keep the expected output in the PHP code block as printed lines or comments.

Check Your Work

Confirm:

  • self::class points at the base class
  • static::class points at the called child class
  • the named constructor returns the child type

Afterward, explain when new static() can be risky.

Show solution
PHP example
<?php

declare(strict_types=1);

class Identifier
{
    public function __construct(
        public readonly string $value,
    ) {
    }

    public static function selfName(): string
    {
        return self::class;
    }

    public static function staticName(): string
    {
        return static::class;
    }

    public static function fromString(string $value): static
    {
        return new static(trim($value));
    }
}

final class OrderId extends Identifier
{
}

echo OrderId::selfName() . PHP_EOL;
echo OrderId::staticName() . PHP_EOL;

$id = OrderId::fromString(' ORD-123 ');

echo $id::class . ': ' . $id->value . PHP_EOL;

// Prints:
// Identifier
// OrderId
// OrderId: ORD-123

new static() can be risky if a child class changes the constructor signature. The inherited factory would still call new static($value), which may no longer be enough to build the child object.