Перейти к содержимому
Поиск
Php Разработка

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

Практический мануал PHP: от переменных и читаемого кода до безопасных форм, интеграций, базы данных и ООП на примере магазина. Учебный маршрут, поиск функций, подробные примеры, разбор ошибок и исходники для скачивания.

Александр НекрасовИзменено:
Практический мануал PHP: от переменных и функций к объектной модели интернет-магазина
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. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.