MidТеория6 min

Анонимные классы

Синтаксис, наследование, интерфейсы, использование в тестах и фабриках

Основы

Анонимные классы (PHP 7.0+) позволяют создавать объекты без предварительного объявления класса. Они полезны для одноразовых объектов -- заглушек в тестах, простых реализаций интерфейсов, lightweight-адаптеров.

<?php
declare(strict_types=1);

// Basic anonymous class
$obj = new class {
    public function greet(): string
    {
        return 'Hello from anonymous class!';
    }
};

echo $obj->greet();  // Hello from anonymous class!

// Type checking
var_dump($obj instanceof stdClass);  // false
var_dump(get_class($obj));           // class@anonymous/path/to/file.php:3$0
var_dump(get_debug_type($obj));      // class@anonymous

Конструктор и аргументы

<?php
declare(strict_types=1);

// Anonymous class with constructor arguments
$user = new class('Alice', '[email protected]') {
    public function __construct(
        private readonly string $name,
        private readonly string $email,
    ) {
    }

    public function toString(): string
    {
        return "{$this->name} <{$this->email}>";
    }
};

echo $user->toString();  // Alice <[email protected]>

// With constructor property promotion
$point = new class(3.0, 4.0) {
    public function __construct(
        public readonly float $x,
        public readonly float $y,
    ) {
    }

    public function distanceFromOrigin(): float
    {
        return sqrt($this->x ** 2 + $this->y ** 2);
    }
};

echo $point->x;                    // 3
echo $point->distanceFromOrigin(); // 5

Наследование и реализация интерфейсов

<?php
declare(strict_types=1);

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

abstract class AbstractLogger implements Logger
{
    abstract protected function write(string $formatted): void;

    public function log(string $message): void
    {
        $formatted = sprintf('[%s] %s', date('Y-m-d H:i:s'), $message);
        $this->write($formatted);
    }
}

// Anonymous class extending abstract class
$consoleLogger = new class extends AbstractLogger {
    protected function write(string $formatted): void
    {
        echo $formatted . "\n";
    }
};

$consoleLogger->log('Application started');

// Anonymous class implementing interface
$nullLogger = new class implements Logger {
    public function log(string $message): void
    {
        // Intentionally empty — null object pattern
    }
};

// Multiple interfaces
interface Countable
{
    public function count(): int;
}

$collection = new class([1, 2, 3]) implements Logger, \Countable {
    public function __construct(
        private readonly array $items,
    ) {
    }

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

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

echo $collection->count();  // 3

// Type checking works with interfaces
var_dump($nullLogger instanceof Logger);  // true
var_dump($collection instanceof \Countable);  // true

// Accept by interface type
function processLogger(Logger $logger): void
{
    $logger->log('Processing...');
}

processLogger($nullLogger);     // OK
processLogger($consoleLogger);  // OK

Практика: Анонимные классы хорошо работают с интерфейсами -- вы можете передавать их везде, где ожидается интерфейс, без создания отдельного именованного класса.

Использование в тестах (Mock/Stub)

<?php
declare(strict_types=1);

interface UserRepository
{
    public function findById(int $id): ?array;
    public function save(array $user): void;
}

interface Mailer
{
    public function send(string $to, string $subject, string $body): bool;
}

class UserService
{
    public function __construct(
        private readonly UserRepository $repository,
        private readonly Mailer $mailer,
    ) {
    }

    public function register(string $name, string $email): array
    {
        $user = ['name' => $name, 'email' => $email, 'id' => random_int(1, 1000)];
        $this->repository->save($user);
        $this->mailer->send($email, 'Welcome!', "Hello, {$name}!");
        return $user;
    }
}

// In test — create inline stubs with anonymous classes
function testRegister(): void
{
    $savedUsers = [];

    $repository = new class($savedUsers) implements UserRepository {
        /** @param list<array> $saved */
        public function __construct(
            private array &$saved,
        ) {
        }

        public function findById(int $id): ?array
        {
            return null;  // Stub
        }

        public function save(array $user): void
        {
            $this->saved[] = $user;
        }
    };

    $sentEmails = [];

    $mailer = new class($sentEmails) implements Mailer {
        /** @param list<array> $sent */
        public function __construct(
            private array &$sent,
        ) {
        }

        public function send(string $to, string $subject, string $body): bool
        {
            $this->sent[] = ['to' => $to, 'subject' => $subject, 'body' => $body];
            return true;
        }
    };

    $service = new UserService($repository, $mailer);
    $user = $service->register('Alice', '[email protected]');

    assert($user['name'] === 'Alice');
    assert(count($savedUsers) === 1);
    assert(count($sentEmails) === 1);
    assert($sentEmails[0]['to'] === '[email protected]');

    echo "Test passed!\n";
}

testRegister();

Использование в фабриках

<?php
declare(strict_types=1);

interface Renderer
{
    public function render(string $template, array $data): string;
}

class RendererFactory
{
    public static function create(string $type): Renderer
    {
        return match ($type) {
            'json' => new class implements Renderer {
                public function render(string $template, array $data): string
                {
                    return json_encode($data, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT);
                }
            },
            'csv' => new class implements Renderer {
                public function render(string $template, array $data): string
                {
                    $output = fopen('php://temp', 'r+');
                    foreach ($data as $row) {
                        fputcsv($output, (array) $row);
                    }
                    rewind($output);
                    $result = stream_get_contents($output);
                    fclose($output);
                    return $result;
                }
            },
            'text' => new class implements Renderer {
                public function render(string $template, array $data): string
                {
                    return str_replace(
                        array_map(fn(string $k): string => "{{$k}}", array_keys($data)),
                        array_values($data),
                        $template,
                    );
                }
            },
            default => throw new InvalidArgumentException("Unknown type: {$type}"),
        };
    }
}

$renderer = RendererFactory::create('text');
echo $renderer->render('Hello, {name}!', ['name' => 'Alice']);
// Hello, Alice!

Scope и доступ к внешним переменным

<?php
declare(strict_types=1);

class Outer
{
    private string $secret = 'hidden';
    protected int $count = 0;

    public function createInner(): object
    {
        // Anonymous class INSIDE a method has access to $this of outer class
        // BUT only through captured variables, not direct scope

        $secret = $this->secret;  // Capture the value

        return new class($secret) {
            public function __construct(
                private readonly string $captured,
            ) {
            }

            public function reveal(): string
            {
                return $this->captured;
            }
        };
    }

    public function createNested(): object
    {
        // Anonymous class defined inside a class method:
        // It does NOT inherit the outer class scope
        // It's a completely separate class

        return new class {
            // Cannot access $this->secret from Outer
            // Cannot access $this->count from Outer

            public function test(): string
            {
                return 'I am independent';
            }
        };
    }
}

$outer = new Outer();
$inner = $outer->createInner();
echo $inner->reveal();  // 'hidden'

Важно: Анонимный класс, определенный внутри метода, НЕ наследует scope внешнего класса. Он не имеет прямого доступа к $this внешнего класса. Переменные нужно передавать явно через конструктор.

get_class() и идентификация

<?php
declare(strict_types=1);

$a = new class {};
$b = new class {};

// Each anonymous class has a unique internal name
echo get_class($a);  // class@anonymous/path/file.php:3$0
echo get_class($b);  // class@anonymous/path/file.php:4$1

// Same definition, same location = same class
function factory(): object
{
    return new class {
        public int $x = 0;
    };
}

$obj1 = factory();
$obj2 = factory();

// Objects from the same anonymous class definition ARE the same class
echo get_class($obj1) === get_class($obj2) ? 'Same' : 'Different';  // Same

// instanceof works
var_dump($obj1 instanceof (get_class($obj2)));  // true

// Comparison
var_dump($obj1 == $obj2);   // true (same class, same property values)
var_dump($obj1 === $obj2);  // false (different instances)

// get_debug_type for cleaner output
echo get_debug_type($a);  // class@anonymous

Вложенные анонимные классы

<?php
declare(strict_types=1);

// Anonymous class returning another anonymous class
$factory = new class {
    public function createLogger(): object
    {
        return new class {
            private array $logs = [];

            public function log(string $message): void
            {
                $this->logs[] = $message;
            }

            /** @return list<string> */
            public function getLogs(): array
            {
                return $this->logs;
            }
        };
    }

    public function createCounter(): object
    {
        return new class {
            private int $count = 0;

            public function increment(): void
            {
                $this->count++;
            }

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

$logger = $factory->createLogger();
$logger->log('First');
$logger->log('Second');
var_dump($logger->getLogs());  // ['First', 'Second']

Когда использовать анонимные классы vs именованные

<?php
declare(strict_types=1);

// USE anonymous classes when:
// 1. One-time use — stub in tests
// 2. Simple interface implementation — 1-3 methods
// 3. Factory pattern with inline variants
// 4. Null Object pattern
// 5. Quick adapter or wrapper

// DO NOT use anonymous classes when:
// 1. Class is reused in multiple places
// 2. Class has complex logic (> ~20 lines)
// 3. Class needs to be type-hinted by name
// 4. Class needs to be serialized/deserialized
// 5. Class needs to be extended

// Good: null object
interface Cache
{
    public function get(string $key): mixed;
    public function set(string $key, mixed $value, int $ttl): void;
}

$nullCache = new class implements Cache {
    public function get(string $key): mixed { return null; }
    public function set(string $key, mixed $value, int $ttl): void {}
};

// Bad: complex logic should be a named class
// $complex = new class {
//     // 100+ lines of business logic...
//     // Hard to test, hard to reuse
// };

Проверь себя

5 из 10

Можно ли наследовать от анонимного класса?

Что вернет `get_debug_type()` для анонимного класса?

Может ли анонимный класс расширять (extends) обычный класс?

Когда НЕ стоит использовать анонимный класс?

Может ли анонимный класс реализовать интерфейс?