Александр Некрасов · Автономное руководство

PHP: полный мануал с примерами, функциями и ООП

Разделы этой страницы
  1. Композиция и DI: собираем сценарий из понятных зависимостей
  2. Клиент использует помощника, которого ему передали
  3. Две связи в коде: хранение и вызов
  4. Время тоже является входом, если оно меняет решение
  5. Обёртка добавляет наблюдение, сохраняя договор
  6. Подготовленный ответ позволяет выполнить сценарий CRM
  7. Один экземпляр помощника или несколько
  8. Из сделки в уведомление: отдельные клиенты и место сборки
  9. Цена явных связей и отказ зависимости
  10. Код и руководство с собой
PHP 8.5.11149 глав4458 карточек справочника
Дерево руководства

Глава 47 · От основ к архитектуре

Композиция и DI: собираем сценарий из понятных зависимостей

Заменяем скрытые globals и контейнер внутри каждого метода явной сборкой. Тестируем скидку, время и уведомление без БД и сети.

Клиент использует помощника, которого ему передали

Клиент CRM должен работать с реальной сетью и с заранее подготовленным ответом при проверке. Не будем создавать транспорт внутри listMethod: передадим его клиенту при создании. Композиция связывает объекты как целое и используемые помощники.

Что получится. Короткий вызов listMethod вернёт сделку 42: Чай для офиса. В учебном запуске всё происходит локально.

Как проходит конкретный вызов
  1. Место сборки создаёт FixtureTransport
  2. Конструктор B24Client сохраняет этот транспорт
  3. listMethod передаёт ему имя метода и параметры
  4. Транспорт возвращает массив ответа
  5. Клиент извлекает result, вызывающий печатает сделку

Интерфейс как заменяемый договор; передача зависимостей конструктору.

Первый помощник без сети: подпись карточкиСамостоятельный пример
<?php
declare(strict_types=1);

class ProductFormatter
{
    public function format(string $title): string { return 'Товар: ' . $title; }
}
class Catalog
{
    public function __construct(private ProductFormatter $formatter) {}
    public function line(string $title): string { return $this->formatter->format($title); }
}
$catalog = new Catalog(new ProductFormatter());
echo $catalog->line('Чай'), "\n";
Ожидаемый результат
Товар: Чай

Catalog не наследует ProductFormatter: каталог использует форматировщик. Передача готового помощника извне называется внедрением зависимости, или DI. Контейнер и фреймворк для этого не нужны.

Две связи в коде: хранение и вызов

Наглядная модель

Сервис получает уведомитель, тест получает запись

Нужно проверить, кому отправлено сообщение о готовности заказа, не отправляя настоящее письмо.

  1. Создаём RecordingNotifier

    Объект хранит пустой массив sent. Он реализует договор Notifier.

  2. Передаём его в OrderReady

    Конструктор сохраняет зависимость. Сервис не ищет её в globals и не создаёт второй уведомитель.

  3. notify получает адрес и A-9

    OrderReady составляет «Заказ A-9 готов» и вызывает send на переданном объекте.

  4. Проверяем тот же массив sent

    В нём одна запись: buyer@example.test и «Заказ A-9 готов». Сети в этом опыте нет.

Внедрение — обычная передача готовой зависимости. Для настоящей доставки при сборке выбирают другую реализацию Notifier; сам сценарий продолжает обращаться к тому же договору.

Запись в примереЧто делает каждый элемент
private ProductFormatter $formatterСохраняет конкретного помощника в поле этого каталога.
new Catalog(new ProductFormatter())Место сборки сначала создаёт помощника, затем передаёт его каталогу.
$this->formatter->format($title)Из текущего каталога берём помощника и вызываем у него метод; результат возвращаем наружу.

Внедрение зависимости — способ получить нужный объект, а композиция — способ организовать обязанности объектов. Не каждая зависимость обязана иметь интерфейс: он полезен при нескольких исполнителях или потребности отделить договор от реализации. Передача всего контейнера скрывает список реальных нужд. В обсуждении контейнера исходная проблема именно в выборе между явными зависимостями и общим доступом ко всем сервисам.

Время тоже является входом, если оно меняет решение

Для проверки срока резерва не надо ждать и менять системные часы. Передадим время как явное значение. Отдельный объект Clock оправдан, когда время нужно нескольким действиям и должно заменяться общей зависимостью; одному сравнению достаточно параметра.

Проверить срок резерва без ожиданияСамостоятельный пример
<?php
declare(strict_types=1);

class Reservation
{
    public function __construct(private int $expiresAt) {}
    public function messageAt(int $now): string
    {
        return $now < $this->expiresAt ? 'Резерв действует' : 'Резерв истёк';
    }
}
$reservation = new Reservation(2000);
echo $reservation->messageAt(1999), "\n";
echo $reservation->messageAt(2000), "\n";
Ожидаемый результат
Резерв действует
Резерв истёк

expiresAt и now измерены в одинаковых условных секундах; в реальном коде это могут быть Unix timestamp. На самой границе срок уже истёк — так сформулирован договор. Проверка не обращается к скрытому time(), поэтому воспроизводится сразу.

Обёртка добавляет наблюдение, сохраняя договор

Хочется видеть, какие методы вызваны, не меняя B24Client. Обёртка реализует Transport, получает другой Transport и передаёт запрос ему. Такой объект часто называют декоратором.

Показать имя вызова перед готовым ответомСамостоятельный пример
<?php
declare(strict_types=1);

interface Transport
{
    public function request(string $method, array $payload): array;
}
class FixtureTransport implements Transport
{
    public function request(string $method, array $payload): array
    {
        return match ($method) {
            'crm.deal.list' => ['result' => [['ID' => '42', 'TITLE' => 'Чай для офиса']]],
            'sendMessage' => ['ok' => true, 'result' => ['message_id' => 7, 'text' => $payload['text']]],
            default => throw new InvalidArgumentException('Неизвестный учебный метод'),
        };
    }
}
class B24Client
{
    public function __construct(private Transport $transport) {}
    public function listMethod(array $parameters, string $method): array
    {
        $response = $this->transport->request($method, $parameters);
        if (!isset($response['result']) || !is_array($response['result'])) {
            throw new UnexpectedValueException('CRM не вернула список');
        }
        return $response['result'];
    }
}

class TracedTransport implements Transport
{
    public function __construct(private Transport $inner) {}
    public function request(string $method, array $payload): array
    {
        echo 'Вызов: ', $method, "\n";
        return $this->inner->request($method, $payload);
    }
}
$b24 = new B24Client(new TracedTransport(new FixtureTransport()));
$deals = $b24->listMethod([], 'crm.deal.list');
echo $deals[0]['TITLE'], "\n";
Ожидаемый результат
Вызов: crm.deal.list
Чай для офиса

B24Client по-прежнему знает только Transport. TracedTransport добавляет действие и возвращает исходный ответ. В журнал не выводим payload целиком: там могут оказаться токены и данные людей. Не добавляйте автоматические повторы платежей без договора идемпотентности.

Подготовленный ответ позволяет выполнить сценарий CRM

Transport обещает request(method, payload): array. FixtureTransport ниже — явно учебный исполнитель: возвращает известную сделку и подтверждение сообщения, не выходит в сеть. match выбирает результат по точному имени метода. Это локальная проверка устройства клиента, а не проверка настоящих API Bitrix24 и Telegram.

Получить сделку коротким вызовом клиентаСамостоятельный пример
<?php
declare(strict_types=1);

interface Transport
{
    public function request(string $method, array $payload): array;
}
class FixtureTransport implements Transport
{
    public function request(string $method, array $payload): array
    {
        return match ($method) {
            'crm.deal.list' => ['result' => [['ID' => '42', 'TITLE' => 'Чай для офиса']]],
            'sendMessage' => ['ok' => true, 'result' => ['message_id' => 7, 'text' => $payload['text']]],
            default => throw new InvalidArgumentException('Неизвестный учебный метод'),
        };
    }
}
class B24Client
{
    public function __construct(private Transport $transport) {}
    public function listMethod(array $parameters, string $method): array
    {
        $response = $this->transport->request($method, $parameters);
        if (!isset($response['result']) || !is_array($response['result'])) {
            throw new UnexpectedValueException('CRM не вернула список');
        }
        return $response['result'];
    }
}

$b24 = new B24Client(new FixtureTransport());
$arrToReq = ['filter' => ['STAGE_ID' => 'NEW'], 'select' => ['ID', 'TITLE']];
$result = $b24->listMethod($arrToReq, 'crm.deal.list');
echo 'Сделка ', $result[0]['ID'], ': ', $result[0]['TITLE'], "\n";
Ожидаемый результат
Сделка 42: Чай для офиса

Имя метода и фильтр передаются явно. Клиент хранит транспорт, передаёт ему параметры и извлекает result. FixtureTransport не применяет фильтр как настоящая CRM: он моделирует уже подготовленный ответ. Код чтения [0] опирается на известную непустую фикстуру; рабочий список надо обрабатывать циклом и проверять отсутствие результатов.

Один экземпляр помощника или несколько

В учебном уведомлении один FixtureTransport передан двум клиентам. Это общий объект, а не две копии. Если транспорт хранит изменяемый токен, пользовательский контекст или буфер запросов, совместное использование требует отдельного решения. В долгоживущем worker данные прошлого задания не должны попадать в следующее. Stateless-помощник без состояния проще безопасно переиспользовать.

Место сборки выбирает время жизни: один объект на операцию, запрос или worker. Composer autoload подключает определения классов по потребности; он не создаёт все сервисы и не выбирает их время жизни. Эти решения делает ваш код или настроенный контейнер.

Из сделки в уведомление: отдельные клиенты и место сборки

Теперь нужен второй клиент, который хранит адресата уведомления. Он использует тот же маленький договор транспорта, но понимает другой ответ. Сначала разобран B24Client; новая часть здесь — только send: собирает chat_id/text, проверяет ok и возвращает номер сообщения.

Подготовить уведомление по полученной сделкеСамостоятельный пример
<?php
declare(strict_types=1);

interface Transport
{
    public function request(string $method, array $payload): array;
}
class FixtureTransport implements Transport
{
    public function request(string $method, array $payload): array
    {
        return match ($method) {
            'crm.deal.list' => ['result' => [['ID' => '42', 'TITLE' => 'Чай для офиса']]],
            'sendMessage' => ['ok' => true, 'result' => ['message_id' => 7, 'text' => $payload['text']]],
            default => throw new InvalidArgumentException('Неизвестный учебный метод'),
        };
    }
}
class B24Client
{
    public function __construct(private Transport $transport) {}
    public function listMethod(array $parameters, string $method): array
    {
        $response = $this->transport->request($method, $parameters);
        if (!isset($response['result']) || !is_array($response['result'])) {
            throw new UnexpectedValueException('CRM не вернула список');
        }
        return $response['result'];
    }
}

class TelegramClient
{
    public function __construct(private Transport $transport, private string $chatId) {}
    public function send(string $text): int
    {
        $response = $this->transport->request('sendMessage', ['chat_id' => $this->chatId, 'text' => $text]);
        if (($response['ok'] ?? false) !== true || !isset($response['result']['message_id'])) {
            throw new UnexpectedValueException('Сообщение не подтверждено');
        }
        return $response['result']['message_id'];
    }
}

$transport = new FixtureTransport();
$b24 = new B24Client($transport);
$telegram = new TelegramClient($transport, 'demo-team');
$deals = $b24->listMethod(['select' => ['ID', 'TITLE']], 'crm.deal.list');
foreach ($deals as $deal) {
    $text = 'Новая сделка ' . $deal['ID'] . ': ' . $deal['TITLE'];
    $messageId = $telegram->send($text);
    echo $text, "\nСообщение ", $messageId, " подготовлено учебным транспортом\n";
}
Ожидаемый результат
Новая сделка 42: Чай для офиса
Сообщение 7 подготовлено учебным транспортом

Полная локальная цепочка выполняется без токенов и сторонних отправок. У реальной сборки B24Client и TelegramClient обычно будут разные HTTP-адаптеры с адресами, авторизацией и ограничениями соответствующих API. Успех этого примера не доказывает сетевую интеграцию.

Цена явных связей и отказ зависимости

Ошибка ответа остаётся видна вызывающему сценариюСамостоятельный пример
<?php
declare(strict_types=1);

interface Transport
{
    public function request(string $method, array $payload): array;
}
class FixtureTransport implements Transport
{
    public function request(string $method, array $payload): array
    {
        return match ($method) {
            'crm.deal.list' => ['result' => [['ID' => '42', 'TITLE' => 'Чай для офиса']]],
            'sendMessage' => ['ok' => true, 'result' => ['message_id' => 7, 'text' => $payload['text']]],
            default => throw new InvalidArgumentException('Неизвестный учебный метод'),
        };
    }
}
class B24Client
{
    public function __construct(private Transport $transport) {}
    public function listMethod(array $parameters, string $method): array
    {
        $response = $this->transport->request($method, $parameters);
        if (!isset($response['result']) || !is_array($response['result'])) {
            throw new UnexpectedValueException('CRM не вернула список');
        }
        return $response['result'];
    }
}

class BrokenTransport implements Transport
{
    public function request(string $method, array $payload): array { return ['error' => 'temporarily_unavailable']; }
}
$b24 = new B24Client(new BrokenTransport());
try {
    $b24->listMethod([], 'crm.deal.list');
} catch (UnexpectedValueException $error) {
    echo 'Уведомление отложено: ', $error->getMessage(), "\n";
}
Ожидаемый результат
Уведомление отложено: CRM не вернула список

Отказ зависимости не превращается в пустой успешный список. Продакшен-адаптер должен различать сетевую ошибку, ответ API с error и действительно пустой result. Локальные исполнители позволяют проверить каждый договор отдельно.

Попробуйте. Добавьте EmptyTransport, возвращающий ["result" => []]. Что выведет цикл уведомления?

Разобрать решение

Цикл не выполнит ни одной итерации, и сообщений не будет. Если пользователю нужно подтверждение отсутствия сделок, сценарий должен явно вывести «Новых сделок нет». Это отличается от ошибочного ответа без result.

Композиция добавляет имена и связи. Для одной чистой функции без состояния она может быть избыточна. Выбирайте её, когда независимая замена помощника и проверка отказов действительно облегчают задачу.

Источники и обсуждения

Код и руководство с собой

Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.

Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.

Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.