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

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

Разделы этой страницы
  1. Интерфейсы: общий договор для оплаты, хранения и тестов
  2. Страница заказа просит действие, а не конкретного исполнителя
  3. Что PHP проверяет и чего договору ещё не хватает
  4. Изменилось требование: тот же checkout принимает другой способ
  5. Не требовать возврат денег от способа, который их ещё не получил
  6. Встроенный потребитель: JSON просит представление объекта
  7. Замена транспорта CRM и границы абстракции
  8. Подменить отказ и увидеть результат для пользователя
  9. Дальше: полный договор языка и специальные случаи
  10. Интерфейс обещает доступное поле, а не способ хранения — PHP 8.4+
  11. Интерфейс может собрать несколько договоров и объявить константы
  12. Два интерфейса принесли одно имя константы: PHP не выбирает за нас
  13. Код и руководство с собой
PHP 8.5.11149 глав4458 карточек справочника
Дерево руководства

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

Интерфейсы: общий договор для оплаты, хранения и тестов

Учим сервис работать с возможностью, а не с названием конкретного провайдера. Ошибки, версии контракта, фейки и ограничения интерфейсов.

Страница заказа просит действие, а не конкретного исполнителя

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

Что получится. Один checkout вернёт Счёт на 25900 коп. или Получить при выдаче: 25900 коп.

Как проходит конкретный вызов
  1. checkout получает Payment и сумму 25900
  2. Тип Payment обещает request(int): string
  3. Конкретный объект формирует свою инструкцию
  4. Страница получает строку одинаковым способом

Объект и метод; параметры и возвращаемый тип.

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

interface Payment
{
    public function request(int $kopecks): string;
}
class InvoicePayment implements Payment
{
    public function request(int $kopecks): string
    {
        return 'Счёт на ' . $kopecks . ' коп.';
    }
}
$payment = new InvoicePayment();
echo $payment->request(25900), "\n";
Ожидаемый результат
Счёт на 25900 коп.

interface объявляет договор; implements связывает класс с ним. В интерфейсе после сигнатуры стоит ;, тела метода нет. Класс предоставляет тело. Этот пример формирует инструкцию и не списывает деньги.

Что PHP проверяет и чего договору ещё не хватает

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

Сценарий знает договор, адаптер знает провайдера

Оформлению заказа нужен результат оплаты. Детали HTTP и конкретного банка не должны попадать в каждый вызов checkout.

  1. checkout передаёт ключ и сумму

    order-8-attempt-1 и 15000 копеек уходят через зависимость типа PaymentGateway.

  2. PaymentGateway: charge → ChargeResult

    Договор задаёт общий вызов. Конкретный объект выбирается при сборке сценария.

    • Место для банковского адаптера

      Его реализация должна перевести договор в запрос банка и проверить ответ.

    • FakePaymentGateway в примере

      Локально возвращает accepted=true или false; реального платежа не выполняет.

  3. Сценарий читает единый результат

    operationId и accepted доступны независимо от внутреннего устройства выбранного адаптера.

Банковский адаптер здесь обозначает место расширения, а исполняемый пример использует два режима fake. Интерфейс задаёт типовой договор; безопасность повторов и смысл ошибок требуют реализации и тестов.

Запись в примереЧто делает каждый элемент
interface PaymentИмя способности, которую потребитель требует от объекта.
request(int $kopecks): string;Обязательная публичная операция: целые копейки на вход, строка на выход.
implements PaymentКласс обязан предоставить совместимые методы, иначе его объявление недопустимо.
Payment $paymentМожно передать любой подходящий объект, не только заранее выбранный InvoicePayment.

PHP проверяет наличие и типы, но не смысл: договору нужны единицы, допустимые значения и поведение при отказе. Наш request готовит инструкцию оплаты и не подтверждает поступление денег. Интерфейс не создают через new, и он не хранит обычное состояние экземпляра. Кроме методов он может задавать константы, а с PHP 8.4 — требования к свойствам; они разобраны ниже.

Изменилось требование: тот же checkout принимает другой способ

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

interface Payment
{
    public function request(int $kopecks): string;
}
class InvoicePayment implements Payment
{
    public function request(int $kopecks): string
    {
        return 'Счёт на ' . $kopecks . ' коп.';
    }
}
class CashOnDelivery implements Payment
{
    public function request(int $kopecks): string
    {
        return 'Получить при выдаче: ' . $kopecks . ' коп.';
    }
}
function checkout(Payment $payment, int $kopecks): string
{
    if ($kopecks < 1) {
        throw new InvalidArgumentException('Сумма должна быть положительной');
    }
    return $payment->request($kopecks);
}

echo checkout(new InvoicePayment(), 25900), "\n";
echo checkout(new CashOnDelivery(), 25900), "\n";
Ожидаемый результат
Счёт на 25900 коп.
Получить при выдаче: 25900 коп.

checkout знает только Payment. При смене исполнителя меняется аргумент в месте сборки, а проверка суммы и вызов request остаются прежними. Никакой общий родитель со случайными полями не требуется.

Не требовать возврат денег от способа, который их ещё не получил

Счёт и наличные при выдаче пока лишь инструкции. Если добавить refund в Payment, часть классов вынуждена будет отвечать «не поддерживается». Лучше отделить способность возврата в RefundsPayments и передавать её только сценарию возврата. Такое разделение небольших договоров называют разделением интерфейсов.

ПотребительНеобходимый договорЧто ему не нужно
Оформление заказаПодготовить инструкцию оплатыВозврат денег, банковская выписка
Сверка поступленийНайти подтверждённые платежиПечать ценника
Возврат оплатыВернуть конкретную подтверждённую операциюНастройки доставки

Класс может implements несколько интерфейсов через запятую. Интерфейс может extends несколько интерфейсов. При совпадении методов требования должны быть совместимы; пустой метод ради устранения ошибки загрузки договор не выполняет по смыслу.

Встроенный потребитель: JSON просит представление объекта

Интерфейс бывает нужен даже без собственного checkout. json_encode знает встроенный JsonSerializable и вызывает jsonSerialize, чтобы получить данные для кодирования. На странице отдаём название и цену, а внутреннюю пометку поставщика исключаем.

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

class ProductCard implements JsonSerializable
{
    public function __construct(
        private string $title,
        private int $priceKopecks,
        private string $supplierNote
    ) {}
    public function jsonSerialize(): array
    {
        return ['title' => $this->title, 'price_kopecks' => $this->priceKopecks];
    }
}
$card = new ProductCard('Чай', 25900, 'Проверить упаковку');
echo json_encode($card, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR), "\n";
Ожидаемый результат
{"title":"Чай","price_kopecks":25900}

Метод возвращает массив, не готовую JSON-строку: кодирует json_encode. JSON_UNESCAPED_UNICODE оставляет читаемую кириллицу, JSON_THROW_ON_ERROR превращает ошибку кодирования в исключение. Такой контроль представления не означает, что весь объект безопасно выдавать автоматически.

Замена транспорта CRM и границы абстракции

Для Bitrix24 интерфейс Transport может обещать request(method, payload): array. HTTP-адаптер отправляет сеть, локальный FixtureTransport возвращает известную запись. B24Client использует один договор и не знает, каким способом получены байты. Важно сохранить одинаковые правила ответа и отказа: fake, который всегда возвращает успех, не проверит обработку тайм-аута.

Попробуйте. Добавьте BankTransfer, возвращающий Перевод: 25900 коп., и используйте существующий checkout.

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

Реализуйте public request(int $kopecks): string и передайте new BankTransfer(). Функция checkout не меняется. Если для нового вида нужны другие обязательные данные, храните их в настроенном исполнителе или пересмотрите общий договор, а не добавляйте специальные if в checkout.

Подменить отказ и увидеть результат для пользователя

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

interface Payment
{
    public function request(int $kopecks): string;
}
class InvoicePayment implements Payment
{
    public function request(int $kopecks): string
    {
        return 'Счёт на ' . $kopecks . ' коп.';
    }
}
class CashOnDelivery implements Payment
{
    public function request(int $kopecks): string
    {
        return 'Получить при выдаче: ' . $kopecks . ' коп.';
    }
}
function checkout(Payment $payment, int $kopecks): string
{
    if ($kopecks < 1) {
        throw new InvalidArgumentException('Сумма должна быть положительной');
    }
    return $payment->request($kopecks);
}

class UnavailablePayment implements Payment
{
    public function request(int $kopecks): string
    {
        throw new RuntimeException('Оплата временно недоступна');
    }
}
try {
    echo checkout(new UnavailablePayment(), 25900), "\n";
} catch (RuntimeException $error) {
    echo 'Заказ не подтверждён. ', $error->getMessage(), "\n";
}
Ожидаемый результат
Заказ не подтверждён. Оплата временно недоступна

Этот исполнитель намеренно моделирует отказ. Сценарий реально вызывает тот же checkout и получает исключение; это проверяет путь отказа без обращения к банку. Исключения в PHP не перечисляются в сигнатуре, поэтому договор ошибок надо описывать рядом с интерфейсом.

Дальше: полный договор языка и специальные случаи

После замены исполнителя можно уточнить интерфейсные свойства PHP 8.4, наследование нескольких договоров и конфликты констант. Сохраняйте имена параметров для именованных вызовов; методы интерфейса публичны.

Разделы ниже сохраняют специальные правила официального PHP Manual: Интерфейс обещает доступное поле, а не способ хранения — PHP 8.4+, Интерфейс может собрать несколько договоров и объявить константы, Два интерфейса принесли одно имя константы: PHP не выбирает за нас.

Интерфейс обещает доступное поле, а не способ хранения — PHP 8.4+

Карточки из CRM и локального каталога должны показывать подпись в одном списке. Ранее мы обещали метод title(); PHP 8.4 также позволяет обещать чтение свойства. Сначала разберите implements и договор методов и виртуальный get.

Одна точка чтения, два источника подписи
  1. Шаблону обещан HasCaption с get caption
  2. LocalCaption хранит Магазин
  3. CrmCaption вычисляет CRM: Заявка 42
  4. Один printCaption печатает оба представления
Один шаблон читает хранимую и вычисляемую подпись — PHP 8.4+Самостоятельный пример
<?php
declare(strict_types=1);

interface HasCaption
{
    public string $caption { get; }
}

class LocalCaption implements HasCaption
{
    public function __construct(public string $caption) {}
}

class CrmCaption implements HasCaption
{
    public string $caption {
        get => 'CRM: ' . $this->title;
    }

    public function __construct(private string $title) {}
}

function printCaption(HasCaption $item): void
{
    echo $item->caption, "\n";
}

printCaption(new LocalCaption('Магазин'));
printCaption(new CrmCaption('Заявка 42'));
Ожидаемый результат
Магазин
CRM: Заявка 42

Get; в интерфейсе означает требование публичного чтения и не содержит реализации. LocalCaption выполняет его обычным публичным свойством, CrmCaption — хуком. Интерфейс не утверждает, что поле нельзя записывать: конкретный LocalCaption дополнительно разрешает запись.

Договор может требовать set или одновременно get и set. Нельзя выполнить публичное требование записи private(set)-свойством: вызывающий код потеряет обещанную возможность. Выбирайте метод, если действие имеет параметры, заметную стоимость или побочные эффекты; интерфейс свойств не заменяет все методы.

В библиотеке с поддержкой PHP 8.2 оставьте caption(): string. Более короткая запись не оправдывает незаметное повышение минимальной версии приложения.

Интерфейс может собрать несколько договоров и объявить константы

Печатной карточке нужны номер и итог. Другой потребитель использует только номер. Два маленьких интерфейса можно сохранить отдельными, а для карточки собрать третий через extends. Это наследование требований, а не копирование реализации.

Составной договор объединяет две возможности
  1. HasOrderNumber требует number()
  2. HasOrderTotal требует total()
  3. OrderDocument extends оба требования
  4. PrintableOrder предоставляет обе реализации
Два требования и собственный формат реализацииСамостоятельный пример
<?php
declare(strict_types=1);

interface HasOrderNumber
{
    public function number(): string;
}

interface HasOrderTotal
{
    public function total(): int;
}

interface OrderDocument extends HasOrderNumber, HasOrderTotal
{
    public const FORMAT = 'text';
    final public const SCHEMA = 'order-v1';
}

class PrintableOrder implements OrderDocument
{
    public const FORMAT = 'csv';
    public function number(): string { return 'ORD-42'; }
    public function total(): int { return 240; }
}

function documentLine(OrderDocument $document): string
{
    return $document->number() . ';' . $document->total();
}

echo documentLine(new PrintableOrder()), "\n";
echo OrderDocument::FORMAT, ', ', PrintableOrder::FORMAT, "\n";
echo PrintableOrder::SCHEMA, "\n";
Ожидаемый результат
ORD-42;240
text, csv
order-v1

Extends у интерфейса перечисляет родительские договоры через запятую. Implements у класса означает выполнение требований. С PHP 8.1 обычную интерфейсную константу можно переопределить; final закрепляет SCHEMA. Константы не хранят уникальное состояние заказа.

Класс может написать implements HasOrderNumber, HasOrderTotal без составного интерфейса. Выделяйте OrderDocument только если у этого набора есть самостоятельный смысл для потребителя. Если два интерфейса объявили одинаковый метод, одна реализация должна быть совместима с обоими договорами, а не произвольно выбрать один.

Методы интерфейса публичны. Имена параметров сохраняйте согласованными: именованный вызов зависит от них, даже если проверка сигнатуры допускает переименование. Конструктор можно объявить в интерфейсе, но PHP.net не рекомендует без необходимости ограничивать этим способ сборки реализаций; чаще потребителю важна работа уже готового объекта.

Два интерфейса принесли одно имя константы: PHP не выбирает за нас

Имплементировать несколько возможностей можно. Но если два независимых договора определили FORMAT с разными значениями, нельзя молча считать, что победил первый в списке. Такое объявление неоднозначно и отклоняется при составлении класса.

Ошибка находится до прикладной работы
  1. CsvRule обещает FORMAT = csv
  2. JsonRule обещает FORMAT = json
  3. Класс пытается унаследовать оба одноимённых значения
  4. PHP отвергает неоднозначное объявление до создания отчёта
BadInterface.php: намеренно несовместимое объявлениеФрагмент или команда
<?php
interface CsvRule { public const FORMAT = 'csv'; }
interface JsonRule { public const FORMAT = 'json'; }
class AmbiguousReport implements CsvRule, JsonRule {}

Этот файл специально неверен. Он не предназначен для включения в рабочую программу. Следующий локальный проверочный запуск получает диагностический отказ отдельного PHP-процесса.

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

$checks = [
    ['BadInterface.php', 'inherits both', 'Конфликт констант подтверждён']
];
foreach ($checks as [$file, $diagnostic, $label]) {
    $process = proc_open(
        [PHP_BINARY, '-n', '-d', 'display_errors=stderr', __DIR__ . '/' . $file],
        [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
        $pipes
    );
    if (!is_resource($process)) {
        throw new RuntimeException('Не удалось запустить учебную проверку');
    }
    fclose($pipes[0]);
    $output = stream_get_contents($pipes[1]);
    $errors = stream_get_contents($pipes[2]);
    fclose($pipes[1]);
    fclose($pipes[2]);
    $exitCode = proc_close($process);
    if ($exitCode === 0 || !str_contains($output . $errors, $diagnostic)) {
        throw new RuntimeException('Ожидаемая ошибка объявления не обнаружена');
    }
    echo $label, "\n";
}
Ожидаемый результат
Конфликт констант подтверждён

Нужен доступный proc_open и файл BadInterface.php рядом с main.php. Запускается только текущий PHP_BINARY и известный учебный файл: shell и пользовательские команды не участвуют. Родитель проверяет ненулевой код завершения и характерную диагностику; обычный try/catch внутри несовместимого класса такую ошибку объявления не исправляет.

Исправлять нужно сам договор. Если это действительно разные характеристики, дайте разные имена константам. Если это выбор формата реализации, один общий интерфейс с методом format() позволит каждому объекту вернуть свой вариант без наследования двух противоречивых констант.

В PHP 8.1+ переопределение обычной унаследованной константы допускается, но не следует случайным значением прятать смысловой конфликт двух интерфейсов. Final-константа запрещает переопределение вовсе. Проверяйте семантику, а не только возможность собрать класс.

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

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

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

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

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