codeisart.

PHP

PHP: Класс-обертка для методов

Обёртка полезна, когда перед обращением к модели нужно подготовить зависимость: например, открыть подключение или создать тяжёлый репозиторий только тогда, когда он…

В этой статье

Обёртка полезна, когда перед обращением к модели нужно подготовить зависимость: например, открыть подключение или создать тяжёлый репозиторий только тогда, когда он действительно понадобится. Важно сохранить обычный контракт методов, возвращаемые значения и понятное поведение при ошибке.

Что меняем в старом приёме

В первоначальном подходе использовались wrapper($method, $data) и магическая переадресация через __call. Но пара [имя_класса, имя_нестатического_метода] не является корректной проверкой обычного вызова на экземпляре. Проверять таким способом конструктор также не нужно: конструктор вызывается оператором new, а его параметры известны фабрике.

Исправлять только опечатку $model-<wrapper на $model->wrapper недостаточно. Нужны договор о типе модели, явный способ её создания и решение о том, что происходит после исключения. Здесь используем типизированный proxy без строковых имён классов и методов.

Один интерфейс для модели и обёртки

Интерфейс ProductRepository описывает ровно доступную операцию. И настоящая модель, и её ленивый заместитель реализуют getName(int $id): string. Вызывающий код не выбирает метод из пользовательского ввода, а IDE и проверка типов видят сигнатуру.

Фабрика передаётся как Closure. Она может захватить заранее настроенные зависимости и параметры конструктора, поэтому обёртка не пытается угадать, как создать произвольный класс. В учебном примере модель работает с массивом: подключение к базе не требуется.

interface ProductRepository
{
    public function getName(int $id): string;
}

final class ArrayProductRepository implements ProductRepository
{
    /** @param array<int, string> $names */
    public function __construct(private array $names)
    {
    }

    public function getName(int $id): string
    {
        if (!array_key_exists($id, $this->names)) {
            throw new OutOfBoundsException('Product was not found.');
        }
        return $this->names[$id];
    }
}

final class LazyProductRepository implements ProductRepository
{
    private ?ProductRepository $instance = null;
    private bool $loading = false;

    /** @param Closure(): ProductRepository $factory */
    public function __construct(private readonly Closure $factory)
    {
    }

    private function repository(): ProductRepository
    {
        if ($this->instance !== null) {
            return $this->instance;
        }
        if ($this->loading) {
            throw new LogicException('Recursive lazy initialization.');
        }
        $this->loading = true;
        try {
            $repository = ($this->factory)();
            if (!$repository instanceof ProductRepository) {
                throw new UnexpectedValueException('Factory must return ProductRepository.');
            }
            return $this->instance = $repository;
        } finally {
            $this->loading = false;
        }
    }

    public function getName(int $id): string
    {
        return $this->repository()->getName($id);
    }
}

Когда создаётся модель

Конструктор proxy только запоминает фабрику. Первый вызов getName обращается к repository(), создаёт модель и проверяет, что фабрика вернула нужный интерфейс. Успешно созданный экземпляр сохраняется; последующие вызовы снова выполняют методы этой модели, но не создают её заново.

Это кэширование зависимости, а не результатов запросов. Два вызова getName(1) по-прежнему дважды вызывают getName репозитория. Если нужно кэшировать сами данные, это отдельная ответственность с собственными ключами и сроком жизни.

$loads = 0;
$products = new LazyProductRepository(
    static function () use (&$loads): ProductRepository {
        ++$loads;
        // Здесь могли бы открываться соединение и создаваться SQL-репозиторий.
        // Для запуска примера достаточно массива, база данных не требуется.
        return new ArrayProductRepository([0 => 'Нулевой товар', 1 => 'Книга']);
    }
);

echo $loads;                 // 0: модель ещё не создана
echo $products->getName(1);  // Книга
echo $products->getName(0);  // Нулевой товар
echo $loads;                 // 1: обе операции используют один экземпляр

try {
    $products->getName(99);
} catch (OutOfBoundsException $error) {
    // Контроллер решает, как представить отсутствие товара пользователю.
    // Обёртка не печатает внутренние ошибки и не выдаёт null вместо результата.
}

Ошибки не должны превращаться в успешный ответ

Обёртка не ловит исключение ради echo и не продолжает работу так, будто операция удалась. Ошибка конструктора или метода доходит до вызывающего слоя. Контроллер может отдельно обработать отсутствие товара, временную недоступность хранилища и непредвиденную ошибку. В HTTP-ответ не нужно выводить SQL, внутренние пути или текст служебного исключения.

Если фабрика завершилась исключением, экземпляр не запоминается. Флаг загрузки сбрасывается в finally, поэтому следующая попытка может повторить инициализацию. При этом повторный вход в ту же фабрику до окончания загрузки немедленно отклоняется как рекурсивная инициализация.

Для настоящего SQL-адаптера подготовку запроса и преобразование инфраструктурных ошибок размещают в самом репозитории либо в отдельном явном декораторе. Ленивому proxy достаточно создать зависимость и передать вызов: смешивать загрузку, SQL, вывод HTML и логирование в один магический диспетчер не требуется.

Границы решения и проверки

  • Фабрика задаётся в коде конфигурации приложения, а не именем класса из запроса.
  • Успешный экземпляр создаётся один раз на объект proxy. Ошибочная попытка не кэшируется.
  • Все публичные методы proxy сохраняют сигнатуры интерфейса; добавление операции требует явной реализации.
  • Защита от рекурсивной загрузки не делает объект механизмом синхронизации потоков или распределённым контейнером.
  • В проверках нужны нулевой идентификатор, отложенное создание, повторное использование, сохранение исходного исключения, повтор после сбоя, неверный результат фабрики и рекурсивный вызов.

Документация

is_callable и callable для экземпляра, интерфейсы объектов PHP, анонимные функции и Closure. Эти разделы объясняют договор вызова; они не заменяют проверку поведения конкретной обёртки.