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

Дерево руководства
Глава 46 · От основ к архитектуре
Трейты: повторное использование кода без скрытых зависимостей
Используем маленькие части поведения, разрешаем конфликты методов и понимаем, почему трейт с собственной БД усложняет приложение.
Одна запись журнала для разных клиентов
Клиенты CRM и Telegram должны одинаково записывать диагностическое событие. Сначала научимся превращать известный массив в строку журнала, затем подключим один метод к двум классам. Трейт — набор членов, включаемых в класс через use.
Что получится. Получим строку {"event":"crm-response","data":{"deal_id":42}} и общий вызов log у обоих клиентов.
- Ответ уже представлен массивом PHP
- json_encode превращает выбранные поля в текст JSON
- use включает общий метод в каждый класс
- Вызов у конкретного клиента использует одну реализацию
<?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-состояние зависит от структуры наследования.
- ExportFormat даёт FORMAT и format
- ExportPresentation использует их в label
- CsvReport получает оба уровня через use
- final у импортированного label ограничивает будущего потомка
<?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; собственное объявление метода непосредственно в потребителе трейта имеет отдельное правило приоритета.
<?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.
- Трейт предоставляет formatLine
- Первый класс делает исходный метод protected
- Render вызывает его из допустимой области
- Псевдоним в другом классе добавляет новое имя, не скрывая старое
<?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.
- Трейт объявил format как string
- Класс объявил тот же format как int
- PHP обнаруживает несовместимые определения
- Нужно изменить модель состояния, а не выбирать метод через as
<?php
trait HasExportFormat { public string $format = 'csv'; }
class BrokenFormat {
use HasExportFormat;
public int $format = 1;
}
Намеренно неверный fixture. Для совместимого одноимённого свойства должны совпадать тип, видимость, readonly и начальное значение.
<?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 по-прежнему отвергает.
- Интерфейс разрешает FORMAT string|null
- Абстрактный родитель сохраняет договор
- Трейт задаёт final string FORMAT для потомка
- Наследование проверяет совместимое уточнение
<?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. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


