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

Дерево руководства
Глава 47 · От основ к архитектуре
Композиция и DI: собираем сценарий из понятных зависимостей
Заменяем скрытые globals и контейнер внутри каждого метода явной сборкой. Тестируем скидку, время и уведомление без БД и сети.
Клиент использует помощника, которого ему передали
Клиент CRM должен работать с реальной сетью и с заранее подготовленным ответом при проверке. Не будем создавать транспорт внутри listMethod: передадим его клиенту при создании. Композиция связывает объекты как целое и используемые помощники.
Что получится. Короткий вызов listMethod вернёт сделку 42: Чай для офиса. В учебном запуске всё происходит локально.
- Место сборки создаёт FixtureTransport
- Конструктор B24Client сохраняет этот транспорт
- listMethod передаёт ему имя метода и параметры
- Транспорт возвращает массив ответа
- Клиент извлекает 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 и не создаёт второй уведомитель.
- 3notify получает адрес и 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.
Композиция добавляет имена и связи. Для одной чистой функции без состояния она может быть избыточна. Выбирайте её, когда независимая замена помощника и проверка отказов действительно облегчают задачу.
Источники и обсуждения
- Интерфейсы PHP
- PSR-11: рекомендуемое использование контейнера
- Контейнер или явные зависимости — обсуждение проблемы
- Глобальная БД мешает тесту — обсуждение проблемы
- Composer: автозагрузка
- Guzzle: использование клиента
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


