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

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

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

Александр НекрасовИзменено:
Практический мануал PHP: от переменных и функций к объектной модели интернет-магазина
PHP 8.5.11149 глав4458 карточек справочника
Дерево руководства

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

Трейты: повторное использование кода без скрытых зависимостей

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

Одна запись журнала для разных клиентов

Клиенты CRM и Telegram должны одинаково записывать диагностическое событие. Сначала научимся превращать известный массив в строку журнала, затем подключим один метод к двум классам. Трейт — набор членов, включаемых в класс через use.

Что получится. Получим строку {"event":"crm-response","data":{"deal_id":42}} и общий вызов log у обоих клиентов.

Как проходит конкретный вызов
  1. Ответ уже представлен массивом PHP
  2. json_encode превращает выбранные поля в текст JSON
  3. use включает общий метод в каждый класс
  4. Вызов у конкретного клиента использует одну реализацию

Массив и JSON; метод класса.

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

$data = ['deal_id' => 42];
$record = ['event' => 'crm-response', 'data' => $data];
echo json_encode($record, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR), "\n";
Ожидаемый результат
{"event":"crm-response","data":{"deal_id":42}}

json_encode выполняет направление массив → JSON-текст. json_decode нужен для обратного направления: полученный JSON → данные PHP. Выбираем необходимые диагностические поля, а не записываем весь ответ с токенами или персональными данными.

Вынести повторяемое действие в трейт

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

Один log включён в два независимых клиента

B24 и Telegram должны одинаково записывать диагностику. Трейт даёт общий код, а каждый класс предоставляет путь к журналам.

  • IntegrationLog — реализация log

    Проверяет канал, готовит строку и дописывает файл; требует метод logDirectory().

    • B24 использует IntegrationLog

      $b24 → log('crm-response', результат) → crm-response.log в переданном каталоге.

    • TelegramClient использует тот же трейт

      $telegram → log('send-status', текст) → send-status.log в переданном каталоге.

Трейт не создаёт третий объект и не превращает B24 в потомка Telegram. В log переменная $this относится к вызванному клиенту. Общая правка метода применяется к обоим классам; секреты в журнал не передают.

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

trait LogLine
{
    public function logLine(string $event, array $data): string
    {
        return json_encode(['event' => $event, 'data' => $data], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
    }
}
class B24Client { use LogLine; }
class TelegramClient { use LogLine; }
echo (new B24Client())->logLine('crm-response', ['deal_id' => 42]), "\n";
echo (new TelegramClient())->logLine('telegram-result', ['message_id' => 7]), "\n";
Ожидаемый результат
{"event":"crm-response","data":{"deal_id":42}}
{"event":"telegram-result","data":{"message_id":7}}

Трейт сам не создаёт объект: new LogLine недопустим. use внутри класса включает реализацию logLine в этот класс. B24Client и TelegramClient не становятся родственниками и не получают общий внешний тип.

Запись в примереЧто делает каждый элемент
trait LogLine { ... }Объявляет повторно используемые члены.
use LogLine; внутри классаВключает трейт. Это другое значение use, чем импорт namespace в начале файла.
$client->logLine(...)Вызывает метод у объекта-потребителя; отдельного объекта трейта нет.

Для одной чистой операции formatLogLine(event, data) обычная функция также подходит. Трейт выбираем, когда именно метод клиента удобен нескольким классам. Он не заменяет интерфейс и не даёт автоматического ускорения.

Добавить запись в фиксированный файл

Теперь log должен сохранять подготовленную строку. Файл один, integration.log рядом с учебной программой. Аргумент event — название разрешённого события, не путь к файлу. FILE_APPEND дописывает запись, LOCK_EX просит исключительную блокировку на время записи. В реальном приложении журнал хранится вне публичного web-каталога; здесь запускайте отдельный учебный файл через CLI.

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

trait IntegrationLog
{
    public function log(string $event, array $data): void
    {
        if (!in_array($event, ['crm-response', 'telegram-result'], true)) {
            throw new InvalidArgumentException('Неизвестное событие журнала');
        }
        $line = json_encode(
            ['event' => $event, 'data' => $data],
            JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
        ) . "\n";
        $written = file_put_contents(__DIR__ . '/integration.log', $line, FILE_APPEND | LOCK_EX);
        if ($written !== strlen($line)) {
            throw new RuntimeException('Запись журнала не завершена');
        }
    }
}

class B24Client { use IntegrationLog; }
class TelegramClient { use IntegrationLog; }
$b24 = new B24Client();
$telegram = new TelegramClient();
$b24->log('crm-response', ['deal_id' => 42]);
$telegram->log('telegram-result', ['message_id' => 7]);
echo file_get_contents(__DIR__ . '/integration.log');
Ожидаемый результат
{"event":"crm-response","data":{"deal_id":42}}
{"event":"telegram-result","data":{"message_id":7}}

Самостоятельный запуск предполагает новый учебный каталог без старого integration.log. В начале проверяется событие, затем кодируется строка, затем она дописывается. strlen измеряет число байт; file_put_contents возвращает число записанных байт или false. Следующий запуск допишет ещё две строки: это назначение журнала. Не кладите сюда токены, пароли и полные ответы с личными данными.

Проверить ошибочные данные до записи

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

trait IntegrationLog
{
    public function log(string $event, array $data): void
    {
        if (!in_array($event, ['crm-response', 'telegram-result'], true)) {
            throw new InvalidArgumentException('Неизвестное событие журнала');
        }
        $line = json_encode(
            ['event' => $event, 'data' => $data],
            JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
        ) . "\n";
        $written = file_put_contents(__DIR__ . '/integration.log', $line, FILE_APPEND | LOCK_EX);
        if ($written !== strlen($line)) {
            throw new RuntimeException('Запись журнала не завершена');
        }
    }
}

class B24Client { use IntegrationLog; }
$b24 = new B24Client();
try {
    $b24->log('../outside', ['deal_id' => 42]);
} catch (InvalidArgumentException $error) {
    echo $error->getMessage(), "\n";
}
try {
    $b24->log('crm-response', ['text' => "\xB1"]);
} catch (JsonException $error) {
    echo "Журнал отклонил некорректный UTF-8\n";
}
$b24->log('crm-response', ['deal_id' => 42]);
echo file_get_contents(__DIR__ . '/integration.log');
Ожидаемый результат
Неизвестное событие журнала
Журнал отклонил некорректный UTF-8
{"event":"crm-response","data":{"deal_id":42}}

in_array с true проверяет точное совпадение разрешённой строки. JSON_THROW_ON_ERROR не позволяет молча записать пустой результат кодирования. Некорректный UTF-8 показан намеренно. Рекурсивные структуры и ресурс тоже требуют политики отказа; не меняйте их на молчаливое удаление данных без объяснения.

Ошибка открытия файла может сопровождаться PHP warning; не скрывайте её оператором @. На рабочем сервере диагностический канал настраивается отдельно, а сценарий решает, блокирует ли ошибка журнала основную операцию. LOCK_EX защищает согласованную запись среди участников, которые соблюдают блокировки; он не даёт ротацию, лимит размера и гарантию доставки в удалённый сборщик.

Явно объявить требование к классу-потребителю

Если трейт зависит от настройки клиента, не угадывайте существование $this->prefix. Потребуйте метод, который её возвращает. abstract в трейте означает обязанность потребителя дать совместимую реализацию.

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

trait ClientLabel
{
    abstract protected function clientName(): string;
    public function label(string $message): string
    {
        return $this->clientName() . ': ' . $message;
    }
}
class B24Client
{
    use ClientLabel;
    protected function clientName(): string { return 'CRM'; }
}
echo (new B24Client())->label('Сделка 42 получена'), "\n";
Ожидаемый результат
CRM: Сделка 42 получена

clientName — явно видимая зависимость общего метода. Если потребитель её не предоставит и не останется abstract, объявление класса ошибочно. Для сложных зависимостей лучше отдельный сервис, переданный конструктору.

Два метода с одним именем: выбрать или дать второе имя

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

trait ShortLabel
{
    public function label(): string { return 'Заказ 42'; }
}
trait DetailedLabel
{
    public function label(): string { return 'Заказ 42: Чай, 25900 коп.'; }
}
class OrderCard
{
    use ShortLabel, DetailedLabel {
        ShortLabel::label insteadof DetailedLabel;
        DetailedLabel::label as detailedLabel;
    }
}
$card = new OrderCard();
echo $card->label(), "\n";
echo $card->detailedLabel(), "\n";
Ожидаемый результат
Заказ 42
Заказ 42: Чай, 25900 коп.

insteadof выбирает реализацию label. as добавляет имя detailedLabel для другого метода; это не переименование с удалением исходного имени. as также умеет адаптировать видимость. Если не разрешить конфликт двух методов, класс нельзя корректно составить.

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

Заменяемый журнал лучше передавать отдельным объектом

Фиксированный файл делает первый запуск понятным. Если одному клиенту нужен файл, другому память, третьему удалённый сборщик, форматирование и запись лучше отделить в Logger. Клиент получает Logger через конструктор; политика отказа и настройки живут в одном месте. Трейт может остаться тонким удобным методом, но не должен скрытно создавать PDO, читать окружение или получать глобальный контейнер.

Конструктор в трейте технически возможен, но способен конфликтовать с конструктором класса. Статические поля трейта тоже требуют внимания: PHP 8.3 изменил разделение такого поля при повторном use у потомка. Не храните в них данные текущего пользователя или запроса.

Проверка полезности перед включением в проект

В нашем IntegrationLog видны входы, фиксированное место записи и отказы. Перед use чужого трейта так же просмотрите свойства, конструктор, magic-методы и обращения к состоянию потребителя. Короткая строка use может добавить много поведения. Если поведение зависит от другой системы, явная композиция облегчает замену и проверку.

Попробуйте. Добавьте разрешённое событие import-finished и запишите число импортированных товаров.

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

Допишите строку в список событий и вызовите log("import-finished", ["imported" => 12]). Не превращайте event в имя файла и не записывайте весь импорт вместо выбранных диагностических полей.

Выбор: функция, трейт, интерфейс или сервис

ЗадачаПодходящий первый вариант
Преобразовать явные данные в строкуФункция
Подключить одну небольшую реализацию метода к нескольким классамТрейт
Обещать одинаковую операцию разным исполнителямИнтерфейс
Менять способ записи и его настройки независимо от клиентаСервис через композицию

Трейт не требует наследования CRM → Telegram и не создаёт тип для параметра функции. Для ограничения потребителя применяется интерфейс; для общей реализации можно дополнительно использовать трейт.

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

Далее уточняются константы, статическое состояние, final, адаптация видимости и конфликты членов. Эти правила сохранены, чтобы общий log не создавал скрытых ошибок в существующем классе.

Разделы ниже сохраняют специальные правила официального PHP Manual: Трейт приносит также константы, состояние и правила переопределения, As меняет доступ или добавляет псевдоним — эти действия различаются, Состояние из трейта должно согласоваться с объявлением класса, PHP 8.5: трейт связывается с классом до присоединения родителя.

Трейт приносит также константы, состояние и правила переопределения

После переиспользуемого метода log важно увидеть границу механизма: трейт вставляет члены в использующий класс. Это не отдельный сервис и не отдельный объект. Поэтому одноимённые свойства и константы должны быть совместимы, а static-состояние зависит от структуры наследования.

Поведение собирается в потребителе
  1. ExportFormat даёт FORMAT и format
  2. ExportPresentation использует их в label
  3. CsvReport получает оба уровня через use
  4. final у импортированного label ограничивает будущего потомка
Вложенный трейт, константа и закрытая точка расширения — PHP 8.3+Самостоятельный пример
<?php
declare(strict_types=1);

trait ExportFormat
{
    public const FORMAT = 'csv';

    public function format(): string
    {
        return self::FORMAT;
    }
}

trait ExportPresentation
{
    use ExportFormat;

    public function label(): string
    {
        return 'Экспорт: ' . $this->format();
    }
}

class CsvReport
{
    use ExportPresentation {
        label as final;
    }
}

echo CsvReport::FORMAT, "\n";
echo (new CsvReport())->label(), "\n";
Ожидаемый результат
csv
Экспорт: csv

Константы трейтов поддерживаются с PHP 8.2; as final — с PHP 8.3. CsvReport получает оба уровня поведения. Final здесь запрещает потомку CsvReport переопределить импортированный label; собственное объявление метода непосредственно в потребителе трейта имеет отдельное правило приоритета.

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

trait CountsRuns
{
    public static int $runs = 0;

    public static function record(): void
    {
        static::$runs++;
    }
}

class ParentImporter
{
    use CountsRuns;
}

class ChildImporter extends ParentImporter
{
    use CountsRuns;
}

ParentImporter::record();
ParentImporter::record();
ChildImporter::record();
echo 'Родитель: ', ParentImporter::$runs, "\n";
echo 'Потомок: ', ChildImporter::$runs, "\n";
Ожидаемый результат
Родитель: 2
Потомок: 1

Именно повторное use в дочернем классе создаёт отдельное свойство в PHP 8.3+. До 8.3 эта комбинация разделяла ячейку. Если потомок просто наследует поле без повторного use или своего объявления, нельзя по этому примеру заключать, что счётчики обязательно независимы.

Вызов static-члена через имя самого трейта устарел с PHP 8.1: обращайтесь через класс-потребитель. Не используйте static-счётчик для общего числа отправлений на сайте — отдельные процессы не разделяют такую память.

Свойство из трейта и одноимённое свойство потребителя совместимы только при совпадении видимости, типа, readonly и начального значения. Для константы должны совпадать видимость, значение и final. Ошибка обнаруживается при составлении класса, до обычного выполнения; это не случай для восстановления внутри того же класса через try/catch.

Если несколько клиентов должны разделять один изменяемый журнал, передайте им один объект логгера. Трейт с полем сам по себе не создаёт такое общее владение. Если настройка нужна разная, свойство каждого объекта хранит собственное состояние; это уже решение модели клиента, а не волшебство use.

As меняет доступ или добавляет псевдоним — эти действия различаются

Трейт содержит вспомогательное форматирование. Класс хочет оставить его для собственного публичного render, но не делать отдельной внешней операцией. После разрешения конфликтов можно адаптировать видимость при use.

Сначала выбрать, что должно быть публичным
  1. Трейт предоставляет formatLine
  2. Первый класс делает исходный метод protected
  3. Render вызывает его из допустимой области
  4. Псевдоним в другом классе добавляет новое имя, не скрывая старое
Изменить исходный доступ и добавить закрытый alias — разные записиСамостоятельный пример
<?php
declare(strict_types=1);

trait FormatsLine
{
    public function formatLine(string $title): string { return '[' . $title . ']'; }
}

class ReportPage
{
    use FormatsLine { formatLine as protected; }

    public function render(): string { return $this->formatLine('Заказы'); }
}

class AliasReportPage
{
    use FormatsLine { formatLine as private internalLine; }
}

$page = new ReportPage();
echo $page->render(), "\n";
try {
    $page->formatLine('Заказы');
} catch (Error $error) {
    echo "Исходный метод закрыт извне\n";
}
echo (new AliasReportPage())->formatLine('Заказы'), "\n";
Ожидаемый результат
[Заказы]
Исходный метод закрыт извне
[Заказы]

As protected без нового имени меняет видимость formatLine. As private internalLine добавляет закрытый псевдоним, а исходный public formatLine остаётся доступным. Ошибка ожидания «я сделал alias private и спрятал метод» может незаметно расширить API класса.

Приоритет остаётся обычным: метод самого класса выше трейта, метод трейта выше унаследованного. Это относится к выбору реализации, но не отменяет final и совместимость обязательных сигнатур. Не рассчитывайте на порядок use как на способ разрешить два конфликтующих определения.

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

После добавления общего трейта два файла по отдельности выглядят допустимыми, а собранный класс перестаёт загружаться. Причина — трейт вставляет свойства и константы в потребителя. Несовместимые определения одного члена нельзя решить методом insteadof.

Составление класса проверяет члены вместе
  1. Трейт объявил format как string
  2. Класс объявил тот же format как int
  3. PHP обнаруживает несовместимые определения
  4. Нужно изменить модель состояния, а не выбирать метод через as
BadTraitProperty.php: конфликт типов одного поляФрагмент или команда
<?php
trait HasExportFormat { public string $format = 'csv'; }
class BrokenFormat {
    use HasExportFormat;
    public int $format = 1;
}

Намеренно неверный fixture. Для совместимого одноимённого свойства должны совпадать тип, видимость, readonly и начальное значение.

BadTraitConstant.php: конфликт значения одной константы — PHP 8.2+Фрагмент или команда
<?php
trait HasExportVersion { public const VERSION = 1; }
class BrokenVersion {
    use HasExportVersion;
    public const VERSION = 2;
}

Константы в трейтах доступны с PHP 8.2. Здесь имя VERSION совпало, а значения нет. Совместимость требует также совпадения видимости и final.

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

$checks = [
    ['BadTraitProperty.php', 'define the same property', 'Конфликт свойства подтверждён'],
    ['BadTraitConstant.php', 'define the same constant', 'Конфликт константы подтверждён']
];
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";
}
Ожидаемый результат
Конфликт свойства подтверждён
Конфликт константы подтверждён

Это диагностический CLI-опыт с двумя сопутствующими файлами и proc_open. Ненулевой код завершения проверяется вместе с содержанием ошибки. Ни один неверный класс не загружается в процесс самого проверяющего сценария.

Если трейт должен пользоваться уже имеющейся настройкой класса, полезнее требовать защищённый метод getFormat() через abstract, а не объявлять второе поле того же имени. Если хранение действительно принадлежит трейту, выберите имя и контракт, согласованные со всеми потребителями.

Передача отдельного объекта зависимости часто уменьшает столкновения: логгер или транспорт хранит собственное состояние, а класс лишь использует его. Трейт хорош для повторения небольшого поведения, но его члены не изолированы от членов потребителя.

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

При переносе старой библиотеки ошибка может возникать в сочетании трёх механизмов: интерфейс задаёт константу, родитель реализует договор, трейт уточняет значение в потомке. С PHP 8.5 трейт сначала входит в собственное объявление класса, затем проверяется наследование. Это делает такой случай похожим на обычное объявление константы прямо в потомке.

Ниже идентификатор формата служит настройкой экспортёра. У родителя он ещё не выбран, а конкретный final-класс задаёт json через трейт. Не смешивайте это с конфликтом двух разных объявлений внутри одной композиции: несовместимые собственные свойства или константы PHP по-прежнему отвергает.

Трейт участвует в собственном объявлении потомка
  1. Интерфейс разрешает FORMAT string|null
  2. Абстрактный родитель сохраняет договор
  3. Трейт задаёт final string FORMAT для потомка
  4. Наследование проверяет совместимое уточнение
Формат экспортёра задан трейт-константойСамостоятельный пример
<?php
declare(strict_types=1);

interface ExportContract
{
    public const ?string FORMAT = null;
}
abstract class BaseExporter implements ExportContract {}
trait UsesJsonFormat
{
    final public const string FORMAT = 'json';
}
final class JsonExporter extends BaseExporter
{
    use UsesJsonFormat;
}

echo JsonExporter::FORMAT, "\n";
echo (new ReflectionClassConstant(JsonExporter::class, 'FORMAT'))
    ->getDeclaringClass()->getName(), "\n";
Ожидаемый результат
json
JsonExporter

Пример требует PHP 8.5; это не обещание того же поведения на прежних версиях с типизированными константами. Трейт включён в JsonExporter, поэтому Reflection указывает этот класс как место объявления итоговой константы. Базовый nullable string уточнён до string.

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

Упражнение: сделайте исходную FORMAT final в интерфейсе. Решение: уточнение станет запрещено независимо от порядка связывания; окончательный договор нельзя обойти через трейт.

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

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

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

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

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