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

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

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

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

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

static, self и parent: класс, объект и позднее связывание

Различаем статический метод, статическое состояние и static-переменную функции. Разбираем фабрики и наследование без скрытого общего состояния.

Составить номер заказа без пустого служебного объекта

Из числа 42 нужно получить номер ORD-42. Операция использует только переданный номер, а личных настроек объекта нет. Её можно написать функцией или статическим методом класса.

Что получится. OrderNumber::format(42) вернёт ORD-42.

Как проходит конкретный вызов
  1. Вызов у имени класса передаёт 42
  2. Статический format получает $id
  3. Возвращает строку ORD-42
  4. echo выводит результат; объект не создавался
Вызвать преобразование у классаСамостоятельный пример
<?php
declare(strict_types=1);

class OrderNumber
{
    public static function format(int $id): string
    {
        return 'ORD-' . $id;
    }
}
echo OrderNumber::format(42), "\n";
Ожидаемый результат
ORD-42

static у метода разрешает вызов без экземпляра. :: выбирает член класса; -> обращается к члену объекта. В этом методе нет $this: никакой текущий заказ ему не передан. Та же операция могла бы быть функцией formatOrderNumber(42).

Запись в примереЧто делает каждый элемент
public static function formatПубличный метод без обязательного экземпляра.
OrderNumber::format(42)Точное имя класса, оператор ::, имя метода и аргумент.
$thisВ статическом контексте отсутствует; настройки надо передать явно либо использовать обычный объект.

Статический метод, поле и локальная переменная имеют разный смысл

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

Два поиска константы из одного класса-потомка

Оба метода вызываются на CsvMetadata. Разница определяется тем, написано внутри self или static.

  • CsvMetadata::declaredFormat()

    Тело метода объявлено в ExportMetadata и использует self::FORMAT.

    • Где искать: ExportMetadata

      self указывает на класс, в котором объявлен этот метод.

    • Результат: generic

      Выбирается константа базового класса, несмотря на вызов через CsvMetadata.

  • CsvMetadata::requestedFormat()

    То же место объявления, но выражение использует static::FORMAT.

    • Где искать: CsvMetadata

      В этом вызове позднее связывание учитывает класс, на котором вызван метод.

    • Результат: csv

      Потомок переопределил константу. Экземпляр объекта для этих вызовов не создавался.

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

ЗаписьНазначениеВремя жизни
static function formatОперация у классаТело вызывается по запросу
static $prefix у классаОбщее поле классаНе принадлежит отдельному экземпляру
static $cache внутри функцииСохранить локальное значение между вызовамиПовторные вызовы в текущем исполнении
static function (...) { ... }Замыкание без автоматического $thisЖивёт как обычное значение замыкания

Одинаковое слово static не делает эти механизмы взаимозаменяемыми. Константа const подходит значению, которое нельзя менять; static-поле остаётся изменяемым. Тип статического метода не обещает чистоту: он технически может писать файлы и сеть, поэтому учитывайте его фактические действия.

self выбирает место объявления, static — вызванный класс

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

class NumberLabel
{
    protected const PREFIX = 'ITEM';
    public static function fixed(int $id): string { return self::PREFIX . '-' . $id; }
    public static function flexible(int $id): string { return static::PREFIX . '-' . $id; }
}
class OrderLabel extends NumberLabel
{
    protected const PREFIX = 'ORD';
}
echo OrderLabel::fixed(42), "\n";
echo OrderLabel::flexible(42), "\n";
Ожидаемый результат
ITEM-42
ORD-42

Оба тела объявлены в NumberLabel. self::PREFIX берёт его константу. static::PREFIX учитывает, что исходный вызов был OrderLabel::flexible, и берёт ORD. Это позднее статическое связывание; оно нужно здесь ради общей операции с разными метаданными потомков.

parent:: выбирает родительскую область; self:: и parent:: могут сохранять вызванный класс для дальнейшего static::. Явное ClassName::method() задаёт свою отправную точку. Подробный пример передачи этого контекста расположен ниже.

Фабрика потомка должна создавать ожидаемый тип

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

class Identifier
{
    final public function __construct(public readonly int $value)
    {
        if ($value < 1) { throw new InvalidArgumentException('Номер должен быть положительным'); }
    }
    public static function fromInt(int $value): static { return new static($value); }
}
class OrderId extends Identifier {}
class ProductId extends Identifier {}
$order = OrderId::fromInt(42);
$product = ProductId::fromInt(7);
echo 'Заказ ', $order->value, ' (', get_class($order), ")\n";
echo 'Товар ', $product->value, ' (', get_class($product), ")\n";
Ожидаемый результат
Заказ 42 (OrderId)
Товар 7 (ProductId)

new static создаёт вызванный класс. : static обещает соответствующий тип результата. final-конструктор сохраняет единый набор аргументов: потомок не может внезапно потребовать другое обязательное значение. Если расширение не нужно, final-класс и new self проще.

Общее изменяемое поле затрагивает несколько объектов

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

class PriceLabel
{
    public static string $currency = 'RUB';
    public function format(int $kopecks): string { return $kopecks . ' ' . self::$currency; }
}
$first = new PriceLabel();
$second = new PriceLabel();
echo $first->format(25900), "\n";
PriceLabel::$currency = 'коп.';
echo $first->format(25900), "\n";
echo $second->format(25900), "\n";
Ожидаемый результат
25900 RUB
25900 коп.
25900 коп.

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

В PHP-FPM пользовательское static-состояние не является общей БД между независимыми запросами. В долгоживущем CLI-worker оно может пережить много заданий. OPcache хранит скомпилированный код и не делает это поле общим надёжным хранилищем.

Статическое замыкание полезно чистому callback

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

$labels = array_map(static fn(int $id): string => 'ORD-' . $id, [42, 43]);
echo implode(', ', $labels), "\n";
Ожидаемый результат
ORD-42, ORD-43

array_map вызывает callback для каждого номера. fn создаёт короткую функцию-значение; static исключает автоматическую привязку $this. Явно использованные внешние значения всё равно могут захватываться по правилам замыканий. Это отдельная возможность, не статическое поле класса.

Когда статический вызов помогает, а когда скрывает зависимость

Чистое format(id) или fromInt(value) легко проверить напрямую. PaymentClient::charge(), который сам читает токен из окружения и отправляет HTTP, скрывает важные входы; настроенный объект с переданным транспортом обычно понятнее. Не делайте все методы static ради короткой записи и не добавляйте объекты ради запрета static.

Попробуйте. Добавьте ProductLabel с PREFIX = SKU и вызовите flexible(7).

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

Наследуйте NumberLabel, переопределите константу и получите SKU-7. Если требуется личный префикс каждого магазина, вместо дальнейшего наследования передайте строку конструктору обычного объекта.

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

Здесь ниже закреплены типизированные константы и точные правила передачи вызываемого класса. Они уточняют уже увиденную разницу ITEM-42 и ORD-42.

Разделы ниже сохраняют специальные правила официального PHP Manual: Константа класса: неизменяемое правило формата, не общий счётчик, Self/parent сохраняют вызванный класс, явное имя задаёт новую отправную точку.

Константа класса: неизменяемое правило формата, не общий счётчик

Экспортёр должен записывать номер поддерживаемой схемы. Это значение относится к формату, а не к конкретному заказу. После разницы объекта и класса удобно явно закрепить правило константой. У const нет знака $; :: выбирает объявление класса.

Правило формата принадлежит классу
  1. SCHEMA закрепляет orders-v1
  2. header читает схему и внутренний разделитель
  3. Получается orders-v1;id
  4. final запрещает потомку переопределить SCHEMA
Типизированная final-константа схемы — PHP 8.3+Самостоятельный пример
<?php
declare(strict_types=1);

class OrderExport
{
    final public const string SCHEMA = 'orders-v1';
    protected const string SEPARATOR = ';';

    public static function header(): string
    {
        return self::SCHEMA . self::SEPARATOR . 'id';
    }
}

$selected = 'SCHEMA';
echo OrderExport::SCHEMA, "\n";
echo OrderExport::{$selected}, "\n";
echo OrderExport::header(), "\n";
Ожидаемый результат
orders-v1
orders-v1
orders-v1;id

String у константы и динамическое ::{$selected} доступны с PHP 8.3. Final у констант — с PHP 8.1: потомок не сможет незаметно назвать другую схему тем же SCHEMA. Protected прячет внутренний разделитель от внешнего вызова.

Номер схемы не становится настоящей миграцией только из-за final. Код чтения обязан знать, что делать с этой версией. Для пользовательского выбора формата сначала проверьте имя по разрешённому списку; динамический доступ не должен раскрывать всё, что названо во входном запросе.

Обычная константа без final может быть переопределена в потомке, а self:: и static:: тогда выбирают значение по разным правилам позднего связывания. Foo::class возвращает имя класса строкой; он не создаёт объект и сам по себе не загружает класс. Const также не заменяет изменяемую конфигурацию окружения и секреты.

Self/parent сохраняют вызванный класс, явное имя задаёт новую отправную точку

Фабрика отчётов должна использовать оформление конкретного потомка. Позднее связывание передаёт информацию о вызванном классе через self:: и parent::. Если вместо них написать точное имя класса, вызов становится другой отправной точкой. Это объясняет, почему замена одной похожей строки может изменить выбор static:: внутри.

Вызванный класс проходит через цепочку
  1. Запущен SpecialReport::variants()
  2. self и parent передают дальше SpecialReport
  3. Явный BaseReport::header начинает цепочку от BaseReport
  4. static::prefix выбирает объявление для соответствующего вызванного класса
Четыре записи вызова выбирают две разные подписиСамостоятельный пример
<?php
declare(strict_types=1);

class BaseReport
{
    public static function prefix(): string { return 'База'; }
    public static function header(): string { return static::prefix(); }
}

class ReportFamily extends BaseReport
{
    public static function variants(): array
    {
        return [BaseReport::header(), parent::header(), self::header(), static::header()];
    }
}

class SpecialReport extends ReportFamily
{
    public static function prefix(): string { return 'Специальный'; }
}

echo implode(', ', SpecialReport::variants()), "\n";
Ожидаемый результат
База, Специальный, Специальный, Специальный

Parent выбирает реализацию метода родителя, но сохраняет вызванный класс SpecialReport для последующего static внутри header. Self выбирает область ReportFamily с тем же сохранением контекста. Явный BaseReport::header фиксирует новую точку вызова BaseReport.

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

class BaseCaption
{
    private function token(): string { return 'Базовая подпись'; }
    public function owned(): string { return $this->token(); }
    public function late(): string { return static::token(); }
}

class CustomCaption extends BaseCaption
{
    private function token(): string { return 'Закрытая подпись потомка'; }
}

$caption = new CustomCaption();
echo $caption->owned(), "\n";
try {
    echo $caption->late(), "\n";
} catch (Error $error) {
    echo "Родитель не получил доступ к private потомка\n";
}
Ожидаемый результат
Базовая подпись
Родитель не получил доступ к private потомка

Owned исполняется в области BaseCaption и вызывает его закрытую реализацию. Static в late пытается разрешить метод через фактически вызванный CustomCaption, но тот token закрыт для области родителя. Late binding меняет разрешение, а не права доступа.

Не выбирайте self или static только из предпочтения синтаксиса. Если алгоритм обязан использовать свою проверенную реализацию, self может быть намеренной границей. Если потомок должен подставлять поведение, static делает это частью контракта и требует совместимости. Для переключения банка или CRM-клиента композиция с интерфейсом часто яснее глубокой статической иерархии.

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

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

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

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

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