Зачем PHP нужны namespace

Namespace в PHP — это часть имени класса, интерфейса, trait, enum, функции или константы. Он не создаёт отдельный модуль в смысле Composer-пакета и сам по себе не загружает файлы. Его задача проще: дать коду уникальные имена и не заставлять весь проект жить в одном глобальном списке User, Config, Logger и Helper.

После Классы, объекты и видимость и Наследование, интерфейсы и трейты это следующий слой организации: класс уже имеет контракт и поведение, а namespace отвечает на вопрос «где этот класс живёт в проекте и как его отличить от одноимённого класса из другой библиотеки?».

<?php

declare(strict_types=1);

namespace App\Billing;

final class Invoice
{
    public function __construct(public readonly int $amount) {}
}

Полное имя класса здесь — App\Billing\Invoice. В другом месте проекта вполне может быть App\Documents\Invoice; PHP не перепутает их, потому что это разные имена.

Быстрое повторение
В файле объявлен `namespace App\Billing;` и класс `Invoice`. Какое полное имя у этого класса?

Объявление namespace в файле

Обычно файл начинается так: <?php, затем declare(strict_types=1);, затем namespace. Перед namespace не должно быть обычного исполняемого кода; declare — нормальное исключение. На практике это хорошо сочетается с правилом «один файл — один основной класс», которое потом подхватывает Autoloading и PSR-4.

<?php

declare(strict_types=1);

namespace App\Catalog;

final class Product
{
    public function __construct(
        public readonly string $sku,
        public readonly string $name,
    ) {}
}

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

src/
  Catalog/
    Product.php        App\Catalog\Product
    ProductRepository.php
  Billing/
    Invoice.php        App\Billing\Invoice

Связь App\Catalog\Productsrc/Catalog/Product.php не является правилом самого языка. Это соглашение автозагрузки, чаще всего PSR-4, и его настраивает Composer. Namespace даёт имя, autoloader решает, из какого файла это имя загрузить.

flowchart TD A[Код в файле: namespace App\\Catalog] --> B{Имя в выражении} B --> C[new Product] B --> D[new \\App\\Billing\\Invoice] B --> E[use App\\Model\\User; new User] B --> F[strlen('php')] C --> C1[App\\Catalog\\Product] D --> D1[App\\Billing\\Invoice] E --> E1[App\\Model\\User] F --> F1[Сначала App\\Catalog\\strlen, затем глобальная strlen] G[Composer PSR-4] --> H[App\\ -> src/] H --> C1 H --> E1
flowchart TD
    A[Код в файле: namespace App\\Catalog] --> B{Имя в выражении}
    B --> C[new Product]
    B --> D[new \\App\\Billing\\Invoice]
    B --> E[use App\\Model\\User; new User]
    B --> F[strlen('php')]
    C --> C1[App\\Catalog\\Product]
    D --> D1[App\\Billing\\Invoice]
    E --> E1[App\\Model\\User]
    F --> F1[Сначала App\\Catalog\\strlen, затем глобальная strlen]
    G[Composer PSR-4] --> H[App\\ -> src/]
    H --> C1
    H --> E1
Как PHP разворачивает имена внутри namespace: текущий namespace, абсолютное имя, импорт через `use` и особое поведение функций.
Быстрое повторение
Что обычно может стоять перед `namespace` в начале PHP-файла, а что не должно?

Полные, абсолютные и относительные имена

В PHP важно различать три формы имени.

Product — неполное имя. Внутри namespace App\Catalog оно обычно означает App\Catalog\Product, если не было импорта через use.

Billing\Invoice — квалифицированное имя без начального слеша. Внутри App\Catalog оно будет интерпретировано как App\Catalog\Billing\Invoice, если первый сегмент не импортирован.

\App\Billing\Invoice — абсолютное имя. Начальный \ говорит: «начинай от глобального пространства имён, не от текущего namespace».

<?php

declare(strict_types=1);

namespace App\Catalog;

$product = new Product('book-1', 'PHP Handbook');
// App\Catalog\Product

$invoice = new Billing\Invoice(1500);
// App\Catalog\Billing\Invoice, если Billing не импортирован

$invoice = new \App\Billing\Invoice(1500);
// App\Billing\Invoice

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

Быстрое повторение
Внутри `namespace App\Catalog;` выполнен код `new Billing\Invoice(1500);`, и `Billing` не импортирован. Как PHP развернёт имя?

use: импорт и alias

use в начале файла создаёт короткое имя для внешнего класса, интерфейса, trait, enum, функции, константы или целого namespace. Это не require и не include: use не читает файл с диска. Он только объясняет компилятору, как разворачивать имя в этом файле.

<?php

declare(strict_types=1);

namespace App\Registration;

use App\Model\User;
use App\Support\EmailNormalizer;
use DateTimeImmutable;
use RuntimeException;
use function App\Support\normalize_email;
use const App\Config\DEFAULT_LOCALE;

final class RegisterUser
{
    public function __invoke(string $email): User
    {
        $normalized = normalize_email($email);

        if ($normalized === '') {
            throw new RuntimeException('Empty email');
        }

        return new User(
            email: $normalized,
            locale: DEFAULT_LOCALE,
            registeredAt: new DateTimeImmutable(),
        );
    }
}

Здесь User разворачивается в App\Model\User, DateTimeImmutable и RuntimeException — в глобальные классы PHP, normalize_email() — в импортированную функцию, а DEFAULT_LOCALE — в импортированную константу. Для функций и констант нужно явно писать use function и use const; обычный use относится к class-like именам: классам, интерфейсам, trait и enum, а также к импортам namespace.

Если два имени конфликтуют, используют alias через as.

<?php

declare(strict_types=1);

namespace App\Import;

use App\Billing\Invoice as BillingInvoice;
use App\Documents\Invoice as DocumentInvoice;

final class ImportResult
{
    public function __construct(
        public readonly BillingInvoice $billing,
        public readonly DocumentInvoice $document,
    ) {}
}

Alias должен помогать читать код, а не прятать происхождение класса. BillingInvoice и DocumentInvoice лучше, чем Invoice1 и Invoice2.

Три разных use, которые не надо путать

Ключевое слово use в PHP перегружено. В namespace оно импортирует имена. В классе оно подключает trait, как в Наследование, интерфейсы и трейты. В замыкании оно захватывает переменные из внешней области, что относится к Функции, замыкания и callable.

use App\Support\Slugger; // импорт имени

final class Post
{
    use HasTimestamps; // подключение trait
}

$prefix = 'post';
$makeSlug = function (string $title) use ($prefix): string {
    return $prefix . '-' . strtolower($title); // захват переменной
};

Одинаковое слово, три разных контекста. Ошибка начинается там, где ожидают, что use App\Support\Slugger «подключит файл» или что use ($prefix) как-то связан с namespace. Нет: это разные механики языка.

Глобальные классы, функции и константы

Внутри namespace неполное имя класса не откатывается в глобальное пространство. Если написать new ArrayObject() внутри App\Catalog, PHP будет искать App\Catalog\ArrayObject, если класс не импортирован.

<?php

declare(strict_types=1);

namespace App\Catalog;

use ArrayObject;

$items = new ArrayObject(['book', 'course']);

Для функций и констант поведение другое: если strlen() или INI_ALL не найдены в текущем namespace, PHP может обратиться к глобальной функции или константе. Несмотря на это, в проектном коде лучше быть последовательным: импортировать проектные функции через use function, а встроенные функции вызывать привычно, если нет риска конфликта.

<?php

declare(strict_types=1);

namespace App\Text;

function strlen(string $value): int
{
    return \strlen($value) + 100;
}

echo strlen('php');  // 103: локальная функция App\Text\strlen
echo \strlen('php'); // 3: глобальная функция PHP

Такой пример полезен для понимания, но в реальном коде переопределять имена встроенных функций без сильной причины не стоит: это сбивает читателя и усложняет тесты.

Практические правила для проекта

Держите namespace близким к директориям. Если Composer настроен как "App\\": "src/", то App\Billing\Invoice естественно живёт в src/Billing/Invoice.php. Это облегчает автозагрузку, поиск по проекту и работу IDE.

Импортируйте зависимости явно. Длинная строка new \Vendor\Package\Deep\Nested\Client() допустима в маленьком примере, но в классе сервиса лучше видеть список зависимостей сверху. Это тот же эффект, что у аккуратных сигнатур интерфейсов: файл сразу показывает, с чем он связан.

Не используйте wildcard-импорты — в PHP их нет для use, и это хорошо. Явные импорты делают конфликты видимыми. Групповой синтаксис допустим, но в большинстве команд читаемее обычный список по одной строке.

use App\Http\{Request, Response};
use App\Security\{CsrfToken, CurrentUser};

Следите за регистром имён. Сам PHP исторически местами терпим к регистру class-like имён, но PSR-4 требует ссылаться на классы case-sensitive, а файл должен совпадать с именем класса. На macOS ошибка может не проявиться локально и всплыть на Linux-сервере.

См. также

Источники

  1. PHP Manual: Пространства имён
  2. PHP Manual: Определение пространств имён
  3. PHP Manual: Псевдонимирование и импорт
  4. PHP Manual: Правила разрешения имён
  5. PHP Manual: Возврат к глобальному пространству для функций и констант
  6. PHP-FIG: PSR-4 Autoloader