PHP-файл: где начинается код

PHP-файл может быть чистым PHP-кодом, а может быть HTML-документом со вставками PHP. Интерпретатор исполняет только то, что находится внутри PHP-тегов. Всё вне тегов считается обычным выводом: в веб-запросе это попадёт в HTTP-ответ, в CLI — в stdout.

Главный открывающий тег — <?php. Его стоит использовать везде: в приложениях, библиотеках, скриптах миграций, конфигах. После <?php нужен пробельный символ, чтобы парсер корректно отделил токены.

<?php

echo "Hello\n";

Короткий echo-тег <?= ... ?> — это сокращение для <?php echo ... ?>. Он полезен в шаблонах:

<h1><?= htmlspecialchars($title, ENT_QUOTES, 'UTF-8') ?></h1>

А вот короткий открывающий тег <? ... ?> лучше не использовать. Его работа зависит от настройки short_open_tag, поэтому такой код хуже переносится между окружениями. Это особенно неприятно в shared hosting, Docker-образах и легаси-проектах, где CLI и FPM могут быть настроены по-разному. Связь с режимами выполнения описана в Версии PHP и режимы выполнения.

Быстрое повторение
В PHP-файле есть HTML вне тегов и блок `<?php ... ?>`. Какая часть реально исполняется интерпретатором PHP?

Закрывающий тег и «невидимый» вывод

В файлах, которые содержат только PHP-код, закрывающий тег ?> обычно опускают. Это не стиль ради стиля: пробел или перевод строки после ?> уже считается выводом. В веб-приложении такой случайный вывод может помешать отправке cookies, редиректа или других заголовков. Подробно это разбирается в HTTP-заголовки, ответы и редиректы.

Хорошо:

<?php

declare(strict_types=1);

function priceWithVat(int $net): int
{
    return (int) round($net * 1.2);
}

Рискованно для library/config-файлов:

<?php

return [
    'debug' => false,
];
?>
 

Последний пробел после закрывающего тега визуально почти не виден, но для PHP это уже байт вывода. Если файл дальше подключается через require, проблема может проявиться далеко от места ошибки.

Быстрое повторение
Почему в PHP-файлах библиотек и конфигов обычно не пишут закрывающий тег `?>`?

Инструкции, комментарии и смешанный HTML/PHP

Инструкции PHP обычно заканчиваются точкой с запятой:

<?php

$name = 'Ada';
echo $name;

Закрывающий PHP-тег автоматически завершает последнюю инструкцию внутри блока, но полагаться на это в обычном PHP-коде не стоит. В шаблонах такая запись встречается, но для бизнес-логики и библиотек точка с запятой делает код понятнее и стабильнее при правках.

PHP поддерживает три формы комментариев:

<?php

// Однострочный комментарий
# Тоже однострочный комментарий, но в современном коде встречается реже

/*
 * Многострочный комментарий.
 * Удобен для пояснения блока, но не должен превращаться в историю файла.
 */

Смешанный HTML/PHP допустим, но требует дисциплины. Шаблон такого вида нормален:

<?php /** @var array<int, string> $items */ ?>
<ul>
    <?php foreach ($items as $item): ?>
        <li><?= htmlspecialchars($item, ENT_QUOTES, 'UTF-8') ?></li>
    <?php endforeach; ?>
</ul>

Здесь используется альтернативный синтаксис управляющих конструкций: foreach (...): ... endforeach;. Он удобен в HTML, потому что закрывающие } хуже читаются среди тегов. Сами циклы и условия подробнее раскрываются в Операторы и управляющие конструкции, а экранирование вывода — в XSS, экранирование вывода и шаблоны.

Быстрое повторение
Зачем в HTML-шаблонах PHP часто пишут `foreach (...): ... endforeach;`, а не фигурные скобки?

include, require и _once

include и require — языковые конструкции для подключения и выполнения другого файла. Это не функции, хотя внешне они похожи на вызов функции.

<?php

$config = require __DIR__ . '/config.php';
require __DIR__ . '/bootstrap.php';
include __DIR__ . '/optional-local-overrides.php';

Практическое различие простое: если файл не найден, include выдаёт warning и выполнение может продолжиться, а require приводит к фатальной ошибке. Поэтому для обязательных файлов приложения обычно используют require: bootstrap, автозагрузчик Composer, конфиг, определения функций. include уместен для действительно необязательных фрагментов, но в современном коде это встречается реже.

require_once и include_once дополнительно проверяют, не был ли файл уже подключён. Это защищает от повторного объявления функций, классов и констант. Но _once не должен быть заменой нормальной архитектуре. В проектах с Composer за загрузку классов обычно отвечает автозагрузчик; см. Autoloading и PSR-4, Неймспейсы и use и Классы, объекты и видимость.

flowchart TD A[Код до подключения] --> B{Подключаемый файл обязателен?} B -- Да --> C[require / require_once] B -- Нет --> D[include / include_once] C --> E{Файл найден?} D --> F{Файл найден?} E -- Нет --> G[Фатальная ошибка: выполнение останавливается] E -- Да --> H[Файл выполняется в текущей области видимости] F -- Нет --> I[Warning: выполнение может продолжиться] F -- Да --> H H --> J{В файле есть return?} J -- Да --> K[Значение возвращается в место подключения] J -- Нет --> L[Успешное подключение обычно возвращает 1]
flowchart TD
    A[Код до подключения] --> B{Подключаемый файл обязателен?}
    B -- Да --> C[require / require_once]
    B -- Нет --> D[include / include_once]
    C --> E{Файл найден?}
    D --> F{Файл найден?}
    E -- Нет --> G[Фатальная ошибка: выполнение останавливается]
    E -- Да --> H[Файл выполняется в текущей области видимости]
    F -- Нет --> I[Warning: выполнение может продолжиться]
    F -- Да --> H
    H --> J{В файле есть return?}
    J -- Да --> K[Значение возвращается в место подключения]
    J -- Нет --> L[Успешное подключение обычно возвращает 1]
Как выбирать между `include`, `require` и вариантами `_once`.

Пути лучше строить от __DIR__, а не от текущей рабочей директории:

<?php

require __DIR__ . '/../vendor/autoload.php';

Так файл найдётся одинаково при запуске из веба, cron, CLI-команды и тестов. Если указать просто require 'vendor/autoload.php';, результат может зависеть от того, откуда был запущен процесс. Для деталей про файловые пути и обёртки см. Файловая система и stream wrappers, а про CLI-контекст — CLI и встроенный сервер.

Область видимости и return из подключаемого файла

Код подключаемого файла выполняется в той области видимости, где стоит include или require. Если подключение находится внутри функции, переменные из подключаемого файла будут локальными для этой функции.

<?php

// config.php
return [
    'timezone' => 'Europe/Berlin',
];
<?php

$config = require __DIR__ . '/config.php';
echo $config['timezone'];

Такой стиль удобен для конфигов: файл не «магически» создаёт переменные $timezone или $db, а явно возвращает значение. Если подключаемый файл не делает return, успешный include/require обычно даёт 1, что редко бывает полезным бизнес-значением.

Не стоит подключать файлы, имя которых напрямую пришло от пользователя:

<?php

// Плохо: пользователь управляет путём к исполняемому PHP-файлу.
require $_GET['page'] . '.php';

Если нужен роутинг, используют whitelist, таблицу маршрутов или фреймворк. Идентификатор из запроса должен выбирать разрешённый вариант, а не становиться частью произвольного пути. Это уже пересекается с GET, POST и фильтрация ввода, SAPI и суперглобалы и Конфигурация безопасности PHP.

declare: директивы файла

declare задаёт директивы выполнения. В повседневном PHP чаще всего встречается strict_types:

<?php

declare(strict_types=1);

function repeatText(string $text, int $times): string
{
    return str_repeat($text, $times);
}

Для strict_types важно место: директива должна быть в начале файла, до исполняемого кода. Обычно её ставят сразу после <?php. Если перед ней окажется BOM, пробел вне PHP-тегов или другой вывод, можно получить ошибку или неожиданный эффект.

Но strict_types=1 не «включает строгий PHP во всём проекте». Как будет проверяться вызов функции, зависит от файла, где этот вызов написан, а не только от файла, где функция объявлена. Подробные правила типов вынесены в Типы и strict_types.

У declare есть и другие директивы, например ticks и encoding, но в обычном веб-приложении они встречаются редко. Если видите declare(ticks=1), это чаще всего связано с обработкой сигналов или старым кодом демонов, а не с типизацией.

Типичные ловушки

Первая ловушка — закрывать ?> в файлах библиотек, а потом искать, почему header() или setcookie() «внезапно» не работают. Вторая — использовать относительный путь без __DIR__ и получать разные результаты в FPM и CLI. Третья — подключать файл ради переменных, которые он создаёт побочным эффектом: через месяц такой код трудно читать и безопасно менять.

Ещё одна частая ошибка — лечить повторные подключения через require_once там, где на самом деле нужен автолоадинг, namespace или нормальный bootstrap. _once полезен, но он не объясняет структуру проекта. Если файл объявляет класс, современный путь почти всегда лежит через Composer и PSR-4.

См. также

Источники

  1. PHP Manual: Основы синтаксиса
  2. PHP Manual: PHP-теги
  3. PHP Manual: Разделение инструкций
  4. PHP Manual: Комментарии
  5. PHP Manual: include
  6. PHP Manual: require
  7. PHP Manual: declare
  8. PHP Manual: Объявления типов
  9. PHP Manual: header