0

Anonymous classes — use cases and limitations

Intermediate5 min read·php-07-017
compare

Concept

Anonymous classes (PHP 7.0+) are class definitions without a name. They're declared inline using new class { ... } syntax and are immediately instantiated. They cannot be referenced by name elsewhere — once the variable holding the instance goes out of scope, the class definition is effectively gone.

Use cases:

  1. Test doubles: Create a quick one-off mock or stub without a dedicated test file.
  2. Interface adapters: Implement an interface inline for a one-time use case without polluting the codebase with a named class.
  3. Callback objects: When you need an invokable object with complex state but don't want a named class at file scope.
  4. Extending for a single test: new class extends SomeClass { protected function hook(): string { return 'test'; } } — override a single protected method without a full subclass.

Limitations: Anonymous classes cannot be serialized (no stable class name for serialize()). They can't be type-hinted by name. They can extend classes, implement interfaces, and use traits — the full class feature set is available.

Under the hood: PHP assigns each anonymous class a unique internal name based on a hash of its definition. This name is visible in stack traces and get_class() output.

Code Example

php
<?php
declare(strict_types=1);

interface Logger
{
    public function log(string $message): void;
}

// Named class approach — creates a file, a class, a name to maintain
class NullLogger implements Logger
{
    public function log(string $message): void {} // no-op
}

// Anonymous class — no file, no name, immediate inline
$logger = new class implements Logger {
    public function log(string $message): void {} // no-op
};

// Extending a class inline (useful in tests)
abstract class BaseController
{
    abstract protected function getTitle(): string;
    public function render(): string { return "<h1>{$this->getTitle()}</h1>"; }
}

$controller = new class extends BaseController {
    protected function getTitle(): string { return 'Test Page'; }
};
echo $controller->render(); // "<h1>Test Page</h1>"

// Test doubles — stub without a mock library
interface UserRepository
{
    public function findById(int $id): ?array;
    public function save(array $user): void;
}

function testWithStub(): void
{
    $users = [1 => ['name' => 'Alice'], 2 => ['name' => 'Bob']];

    $repo = new class($users) implements UserRepository {
        public function __construct(private array $data) {}
        public function findById(int $id): ?array { return $this->data[$id] ?? null; }
        public function save(array $user): void { $this->data[] = $user; }
    };

    assert($repo->findById(1)['name'] === 'Alice');
}

// Anonymous class with constructor arguments
function createFormatter(string $prefix, string $suffix): object
{
    return new class($prefix, $suffix) {
        public function __construct(
            private string $prefix,
            private string $suffix,
        ) {}

        public function format(string $value): string
        {
            return $this->prefix . $value . $this->suffix;
        }
    };
}
$boldFormatter = createFormatter('<b>', '</b>');
echo $boldFormatter->format('Hello'); // "<b>Hello</b>"