В этой статье я хочу поближе познакомить вас с замечательной фичей — Phar-архивами. В предыдущем посте я упоминал о ней, а много раньше даже писал обзорную статью по Phar. Целью этого поста будет полная упаковка Zend Framework 2 в один архив, чтобы раз и навсегда исключить проблему инклюдов в ZF веб-приложениях.В последнее время я слышу слишком много флуда, что «PHP уже не торт, вот в Ruby, там да…». Или «на PHP кодят только школьники», «Enterprise приложение на PHP не сделаешь», «ZF — тормозной фреймворк» ну и т.д. А ведь проблемы то растут не из технологий. Хотя PHP имеет груз обратной совместимости, и ZF1 объективно из коробки показывает не высокую производительность. Однако, при использовании драйвера «прямые руки» можно спокойно и без нервов переписать тормозящий участок кода, работать через ORM, оптимизировать код и т.д. Но многим людям проще обвинить PHP, чем заняться делом. Это печально 

В этом посте я хочу рассказать об устранении одной из проблем ZF1 -огромного количество инклюдов (и как следствие файловых операций), а именно компиляция приложения в PHAR архив. Не скажу, что технология компиляции php кода в один файл новая. Раньше были попытки сделать это, но получалось прямо скажем не всегда. Приходилось в полуавтоматическом режиме вырезать require_once() из кода, а в некоторых местах ZF1 он был нужен, писались исключения. В общем этот подход лично я решил не применять, а ограничился установкой APC байткод-кэшера.
Однако с выходом php 5.3.0 появилась замечательная возможность использовать обертку потока phar://. Теперь можно повторить эксперимент на новом уровне.
Сразу оговорюсь, что можно точно также сжать и Zend Framework 1, есть всего-лишь одно небольшое отличие и я расскажу о нём чуть ниже. Для знакомства с теорией рекомендую почитать вот эту статьюна Хабре. Для упаковки ZF2 в Phar нам понадобится несколько компонентов:
- Собственно сам ZF2. Скачать последнюю версию можно с офсайта или из Git репозитария.
- Файл-загрузчик для Phar-архива (stub.php) — заглушка. Получает управления сразу после инклюда файла с архивом. Будет ниже.
- Упаковщик package.php. Будет ниже.
Stub.php для Zend Framework 1.x
1.
2.
require_once
dirname(
__FILE__
).
'/Zend/Loader/Autoloader.php'
;
3.
Zend_Loader_Autoloader::getInstance();
4.
Zend_Loader_Autoloader::getInstance()->setFallbackAutoloader(true);
5.
// Finalize stumb
6.
__HALT_COMPILER();
Stub.php для Zend Framework 2.x
1.
2.
require_once
dirname(
__FILE__
) .
'/Zend/Loader/AutoloaderFactory.php'
;
3.
Zend\Loader\AutoloaderFactory::factory(
array
(
'Zend\Loader\StandardAutoloader'
=>
array
()));
4.
$moduleLoader
=
new
Zend\Loader\ModuleAutoloader();
5.
$moduleLoader
->register();
6.
7.
// Finalize stumb
8.
__HALT_COMPILER();
Package.php
001.
002.
ini_set
(
'phar.readonly'
, 0);
003.
/**
004.
* package.php
005.
* Create a Zend Framework phar
006.
*
007.
* @author Cal Evans
008.
* @author John Douglass
009.
*/
010.
011.
$getOptLongArray
=
array
(
"stub:"
);
012.
$getOptParams
=
"s:p:v"
;
013.
$options
=
getOpt
(
$getOptParams
,
$getOptLongArray
);
014.
015.
if
(!isset(
$options
[
's'
],
$options
[
'p'
]))
016.
{
017.
echo
"You did not specify either a path or a phar file name.\n"
;
018.
displayHelp();
019.
die
(1);
020.
}
021.
022.
/*
023.
* Set up our environment
024.
*/
025.
$sourceLocation
=
$options
[
's'
];
026.
$basePointer
=
strpos
(
$options
[
's'
],
'Zend'
);
027.
$pharFile
=
$options
[
'p'
];
028.
029.
/*
030.
* Make sure things are sane before progressing
031.
*/
032.
if
(
$basePointer
<1 code="">1>
033.
echo
"It looks like your path is not a Zend Framework path.\nPlease check and try again.\n"
;
034.
displayhelp();
035.
die
(1);
036.
}
037.
038.
// At this point, we need to check to see if the file exists. If neither exist, throw exception.
039.
if
(isset(
$options
[
'stub'
])) {
040.
$stubFile
=
$options
[
'stub'
];
041.
}
else
{
042.
$stubFile
=
'stub.php'
;
043.
}
044.
045.
if
(!
file_exists
(
$sourceLocation
))
046.
{
047.
echo
"ERROR: Source file location does not exist!\nCheck your source and try again.\n"
;
048.
displayhelp();
049.
die
(1);
050.
}
051.
052.
/*
053.
* Let the user know what is going on
054.
*/
055.
echo
"Creating PHAR\n"
;
056.
echo
"Source : {$sourceLocation}\n"
;
057.
echo
"Destination : {$pharFile}\n"
;
058.
echo
"Stub File : {$stubFile}\n\n"
;
059.
060.
/*
061.
* Clean up from previous runs
062.
*/
063.
if
(
file_exists
(
$pharFile
)) {
064.
Phar::unlinkArchive(
$pharFile
);
065.
}
066.
067.
/*
068.
* Setup the phar
069.
*/
070.
$p
=
new
Phar(
$pharFile
, 0,
$pharFile
);
071.
$p
->compressFiles(Phar::GZ);
072.
$p
->setSignatureAlgorithm (Phar::SHA1);
073.
074.
/*
075.
* Now build the array of files to be in the phar.
076.
* The first file is the stub file. The rest of the files are built from the directory.
077.
*/
078.
$files
=
array
();
079.
$files
[
"stub.php"
] =
$stubFile
;
080.
081.
echo
"Building the array of files to include.\n"
;
082.
083.
$rd
=
new
RecursiveIteratorIterator(
new
RecursiveDirectoryIterator(
$sourceLocation
));
084.
foreach
(
$rd
as
$file
) {
085.
if
(
strpos
(
$file
->getPath(),
'.svn'
)===false &&
086.
$file
->getFilename() !=
'..'
&&
087.
$file
->getFilename() !=
'.'
)
088.
{
089.
$fileIndex
=
substr
(
$file
->getPath().DIRECTORY_SEPARATOR.
$file
->getFilename(),
$basePointer
);
090.
$fileName
=
$file
->getPath().DIRECTORY_SEPARATOR.
$file
->getFilename();
091.
$files
[
$fileIndex
] =
$fileName
;
092.
// Coined "phindex" to refer to the included index pointing to the real filename on disk we are creating
093.
if
(isset(
$options
[
'v'
])) {
094.
echo
" PHIndex[{$fileIndex}] = {$fileName}\n"
;
095.
}
// if (isset($options['v']))
096.
}
097.
}
// foreach($rd as $file)
098.
099.
echo
"Now building the phar.\n"
;
100.
101.
/*
102.
* Now build the archive.
103.
*/
104.
$p
->startBuffering();
105.
$p
->buildFromIterator(
new
ArrayIterator(
$files
));
106.
$p
->stopBuffering();
107.
108.
/*
109.
* finish up.
110.
*/
111.
$p
->setStub(
$p
->createDefaultStub(
"stub.php"
));
112.
$p
= null;
113.
114.
if
(isset(
$options
[
'v'
])) {
115.
echo
count
(
$files
).
" files Added to "
.
$pharFile
.
"\n"
;
116.
}
// if (isset($options['v']))
117.
118.
echo
"Done.\n"
;
119.
exit
;
120.
121.
function
displayHelp()
122.
{
123.
echo
"\n\npachage.php\n"
;
124.
echo
" Authors: Cal Evans, John Douglass\n\n"
;
125.
echo
" -s The directory where Zend Framework is located. Must end in /Zend. \n"
;
126.
echo
" -p The name to give your phar file.\n"
;
127.
echo
" --stub The name of your stub file. Will default to stub.php if not passed in.\n"
;
128.
echo
" -v verbose mode.\n"
;
129.
}
Теперь надо правильно расположить файлы. В каталоге вашего проекта создайте папку ./vendor/ZendFramework/library Поместите туда эти файлы (stub.php, package.php) и папку Zend с самим фреймворком. Такое расположение папок характерно для ZF2. Если у вас в проекте другое, то придётся подправить пути. Дальше запускайте следующую команду для компиляции ZF в один файл.
01.
andrey@z11:~/sandbox/zf2/vendor/ZendFramework/library$ php ./package.php -s ./Zend -p zf.phar -
v
02.
Creating PHAR
03.
Source : ./vendor/ZendFramework/library/Zend
04.
Destination : zf.phar
05.
Stub File : stub.php
06.
PHP Fatal error: Uncaught exception 'UnexpectedValueException' with message 'creating archive
"zf.phar"
disabled by the php.ini setting phar.
readonly
'
in
/home/andrey/zf2.ru/package.php:70
07.
Stack trace:
08.
#0 /home/andrey/zf2.ru/package.php(70): Phar->__construct('zf.phar', 0, 'zf.phar')
09.
#1 {main}
10.
thrown
in
/home/andrey/zf2.ru/package.php on line 70
Чтобы убрать эту ошибку надо разрешить запись в phar-архивы в php.ini:
1.
[Phar]
2.
phar.
readonly
= 0
Теперь весь ZF упакован в один файл:
1.
...
2.
PHIndex[ZendFramework/library/Zend/Mail/Transport/Exception.php] = ./vendor/ZendFramework/library/Zend/Mail/Transport/Exception.php
3.
PHIndex[ZendFramework/library/Zend/Mail/Transport/Sendmail.php] = ./vendor/ZendFramework/library/Zend/Mail/Transport/Sendmail.php
4.
PHIndex[ZendFramework/library/Zend/Mail/Storage.php] = ./vendor/ZendFramework/library/Zend/Mail/Storage.php
5.
Now building the phar.
6.
2836 files Added to zf.phar
7.
Done.
Отлично, теперь в каталоге ./vendor/ZendFramework/library/ у вас должен появиться файл zf.phar. А значит можно подключать скомпиленный ZF к проекту.
test_phar.php. Подключение для ZF2:
1.
2.
include
(
"phar://"
.dirname(
__FILE__
).
"/vendor/ZendFramework/library/zf.phar"
);
3.
$version
=
new
Zend\Version;
4.
print
"Compiled ZF version is: \r\n"
;
5.
print
$version
::VERSION.
"\r\n"
;
test_phar.php Подключение для ZF1:
1.
2.
include
(
"phar://"
.dirname(
__FILE__
).
"/vendor/ZendFramework/library/zf.phar"
);
3.
$version
=
new
Zend_Version;
4.
print
"Compiled ZF version is: \r\n"
;
5.
print
$version
::VERSION.
"\r\n"
;
Однако при запуске test_phar.php отображается пустой экран. При этом в логи PHP ничего не пишет. Прогуглив как следует этот вопрос, я выяснил, что это происходит из-за Suhosin patch. Для того, чтобы Phar архивы читались нормально, если в системе стоит suhosin patch, вам надо прописать в /etc/php5/conf.d/suhosin.ini (или в php.ini) следующее:
1.
suhosin.executor.include.whitelist=
"phar"
После этого скрипт должен вывести версию ZF, подключённого через phar архив.
1.
andrey@z11:~/sandbox/zf2.ru
# php ./test_phar.php
2.
Compiled ZF version is:
3.
2.0.0beta1
Да, теперь весь ZF можно подключить с помощью одного include! Вы кстати можете модифицировать package.php для своего проекта и вообще залить всё приложение (со всеми библиотеками в один файл). А для хостингов вообще можно красиво сделать. Компилируем php-фреймворки или CMS’ки в phar, включаем APC, и один экземпляр фреймворка в памяти шарится для всех клиентов.
Кэширование APC и PHAR
Самое замечательное в этой ситуации, что когда фреймворк инклюдится по одному файлу (см. рис ниже), то часть из них всё равно вываливается из кэша, а тут один файл, который априори будет всегда лежать в памяти (т.к. будет инклюдится при каждом запросе). Вот что происходит при обычном выполнении запросов.
Теперь для чистоты экспериментов очистим кэш.
Сделаем include PHAR архива и посмотрим кэш.
А потом ещё раз include.
О чём это говорит? При include конкретного файла класса он сначала берется из кэша, и если его там нет, то из Phar архива, который тоже висит в памяти, т.е. также кэшируется. В итоге дисковых операций не происходит.
Бонусы

zf_1_11_0_dev.phar.tar.gz (зеркало)
zf_2_0_0beta1.phar.tar.gz (зеркало)
Скачать всё с GitHub
Подключаются они обычным инклюдом:
1.
2.
include
(
"phar://"
.dirname(
__FILE__
).
"/vendor/ZendFramework/library/zf.phar"
);
Не забудьте, что при перекомпилировании фреймворка при работающем APC в памяти остаётся висеть старый архив, и вам надо перезапустить php5-fpm или Apache, чтобы сбросить кэш.
Ссылки
- http://www.php.net/manual/en/phar.using.intro.php
- http://blog.calevans.com/2009/07/19/lessons-in-phar/
- http://habrahabr.ru/blogs/php/118269/
UPD
Обязательно нужно убрать саму папку library/Zend из проекта, иначе идет передекларирование классов, т.е. ты все классы подключил через phar, а Loader делает include_once например, и он инклудит физический файл, но класс уже был объявлен в phar и выходит ошибка. Или же проверять загружен ли класс или нет перед include_once.